Converging the state as defined by config.
Dev

Dev commited on 2018-06-16 23:34:21
Showing 8 changed files, with 154 additions and 74 deletions.

... ...
@@ -1,7 +1,3 @@
1
-- log:
2
-  level: info
3
-  device: STDOUT
4
-
5 1
 - packages:
6 2
   - name: apache2
7 3
     version: 2.4.7-1ubuntu4.20
... ...
@@ -21,6 +21,6 @@
21 21
   - name: apache2
22 22
     dependencies:
23 23
     - packages:
24
-      - name: libapache2-mod-php5
24
+      - name: libapache2-mod-php
25 25
     - files:
26 26
       - path: /tmp/hello_world.php
... ...
@@ -16,6 +16,7 @@ module RCM
16 16
     def apply_attributes(file)
17 17
       raise 'apply_attributes only works with ::RCM::File' unless file.is_a?(::RCM::File)
18 18
 
19
+      @logger.debug("Setting owner as #{file.owner}:#{file.group} and mode as #{file.mode} on #{file.path}")
19 20
       # Will throw exception if something poopy happens
20 21
       ::FileUtils.chown(file.owner, file.group, file.path)
21 22
       ::FileUtils.chmod(file.mode, file.path)
... ...
@@ -26,7 +27,9 @@ module RCM
26 27
 
27 28
     def copy(file)
28 29
       dir = ::File.dirname(file.path)
30
+      @logger.debug("Ensuring #{dir} exsits.")
29 31
       ::FileUtils.mkdir_p(dir)
32
+      @logger.debug("Copying #{file.src_path} to #{file.path}.")
30 33
       ::FileUtils.copy(file.src_path, file.path, remove_destination: true)
31 34
 
32 35
       file.changed = true
... ...
@@ -34,9 +37,11 @@ module RCM
34 37
     end
35 38
 
36 39
     def remove(file)
37
-      file_existed = ::File.file?(file.path)
40
+      return unless ::File.file?(file.path)
41
+
42
+      @logger.debug("Removing #{file.path}.")
38 43
       ::FileUtils.rm(file.path)
39
-      file.changed = true if file_existed
44
+      file.changed = true
40 45
       file
41 46
     end
42 47
 
... ...
@@ -11,7 +11,7 @@ module RCM
11 11
       @logger = logger
12 12
     end
13 13
 
14
-    def update()
14
+    def update
15 15
       @logger.info('Updating apt cache.')
16 16
       unless ::File.readable?(APT_PARTIAL_DIR)
17 17
         @logger.warn('Skipping apt cache update as we are not running with right privilege.')
... ...
@@ -27,36 +27,30 @@ module RCM
27 27
       ::RCM.cmd("#{DPKG_QUERY} --status #{pkg_name}")
28 28
     end
29 29
 
30
-    def install(pkg_name, version = '')
31
-      raise "\n\nPlease ensure you are running with root privileges" unless ::File.readable?(APT_PARTIAL_DIR)
32
-
33
-      name = ''
34
-      version_string = "=#{version}" unless version.empty?
35
-      if pkg_name.is_a?(String)
36
-        name = pkg_name
37
-        version_string = "=#{version}" unless version.empty?
38
-      elsif pkg_name.is_a?(::RCM::Package)
39
-        name = pkg_name.name
40
-        v = pkg_name.version
41
-        version_string = "=#{v}" unless v.empty?
42
-      end
43
-
44
-      ::RCM.cmd("#{APT_GET} install --yes #{name}#{version_string}")
45
-    end
46
-
47
-    def multi_install(pkgs)
30
+    def install(pkgs)
48 31
       raise "\n\nPlease ensure you are running with root privileges\n\n" unless ::File.readable?(APT_PARTIAL_DIR)
49 32
 
50
-      raise 'multi_install expects array of ::RCM.Package' unless pkgs.is_a?(Array)
33
+      # If we only get one obejct, add to it an array and process it like that
34
+      packages = pkgs.is_a?(::RCM::Package) ? [pkgs] : pkgs
51 35
 
36
+      @logger.info("Installing #{packages.join(', ')}")
52 37
       # Construct string of packages to install in one go.
53 38
       pkgs_string = ''
54
-      pkgs.each do |pkg|
55
-        version_string = "=#{pkg.version}" unless pkg.version.empty?
39
+      packages.each do |pkg|
40
+        version_string = pkg.version.empty? ? '' : "=#{pkg.version}"
56 41
         pkgs_string += "#{pkg.name}#{version_string} "
57 42
       end
58 43
 
59
-      ::RCM.cmd("#{APT_GET} install --yes #{pkgs_string}")
44
+      cmd_string = "#{APT_GET} install --yes #{pkgs_string}"
45
+      status = ::RCM.cmd(cmd_string)
46
+      if status[:exit_code] == 0
47
+        @logger.info("Success: #{cmd_string}.")
48
+        # Update state
49
+        packages.each { |pkg| pkg.changed = true; pkg.state = ::RCM::Package::INSTALLED }
50
+      else
51
+        @logger.error("Failed: #{cmd_string}\n\nstderr:\n#{status[:error]}\n\nstdout:\n#{status[:output]}")
52
+      end
53
+
60 54
     end
61 55
 
62 56
     def get_versions(pkg_name)
... ...
@@ -75,13 +69,16 @@ module RCM
75 69
       available_versions.uniq
76 70
     end
77 71
 
78
-    def remove(pkg_name)
72
+    def remove(pkg)
79 73
       raise "\n\nPlease ensure you are running with root privileges\n\n" unless ::File.readable?(APT_PARTIAL_DIR)
80 74
 
75
+      @logger.info("Uninstalling #{pkg.name}.")
81 76
       # Remove is idempotent and returns with success even if package is not installed.
82
-      status = ::RCM.cmd("#{APT_GET} remove --yes #{pkg_name}")
83
-      error_message = "Removing #{pkg_name} failed!\n\nstderr:\n#{status[:error]}\n\nstdout:\n#{status[:output]}"
84
-      raise error_message unless status[:exit_code] == 0
77
+      status = ::RCM.cmd("#{APT_GET} remove --yes #{pkg.name}")
78
+      @logger.debug("\nstderr:\n#{status[:error]}\n\nstdout:\n#{status[:output]}")
79
+      raise "Removing #{pkg.name} failed!" unless status[:exit_code] == 0
80
+      pkg.changed = true
81
+      pkg.state = ::RCM::Package::REMOVED
85 82
     end
86 83
 
87 84
     def get_current_state(wanted_packages)
... ...
@@ -92,7 +89,7 @@ module RCM
92 89
         status = status(p.name)
93 90
 
94 91
         if status[:exit_code] == 0
95
-          state = ::RCM::Package::INSTALLED if status[:output] =~ 'ok installed'
92
+          state = ::RCM::Package::INSTALLED if status[:output] =~ /.+ok installed/
96 93
           version = /Version: (?<version>\d+.+)/.match(status[:output])[:version]
97 94
         end
98 95
 
... ...
@@ -1,13 +1,34 @@
1
-require 'logger'
2
-
3 1
 module RCM
4 2
   class ServiceManager
3
+    SYS_SERVICE_MGR = '/usr/sbin/service'.freeze
5 4
     def initialize(logger)
6 5
       @logger = logger
7 6
     end
8 7
 
9
-    def self.dependencies_changed?(service)
10
-      # service.depn
8
+    def dependencies_changed?(service)
9
+      service.depends_file.values.each do |f|
10
+        if f.changed
11
+          @logger.debug("Dependencies for service '#{service.name}' changed because #{f.path} changed.")
12
+          return true
13
+        end
14
+      end
15
+
16
+      service.depends_package.values.each do |p|
17
+        if p.changed
18
+          @logger.debug("Dependencies for service '#{service.name}' changed because #{p.name} changed.")
19
+          return true
20
+        end
21
+      end
22
+
23
+      @logger.info("Dependencies for service '#{service.name}' have not changed.")
24
+      false
25
+    end
26
+
27
+    def restart(service)
28
+      @logger.debug("Restarting #{service.name}")
29
+      status = ::RCM.cmd("#{SYS_SERVICE_MGR} #{service.name} restart")
30
+      error_msg = "Failed to restart #{service.name}.\n\nstderr:\n#{status.error}\n\nstdout:\n#{status.output}"
31
+      @logger.fatal(error_msg)
11 32
     end
12 33
   end
13 34
 end
... ...
@@ -1,11 +1,10 @@
1 1
 module RCM
2 2
   class Package
3
-    attr_accessor :name, :version, :state
3
+    attr_accessor :name, :version, :state, :changed
4 4
 
5 5
     # Public state constants
6 6
     INSTALLED = 'installed'.freeze
7 7
     REMOVED = 'removed'.freeze
8
-    CHANGED = 'changed'.freeze
9 8
 
10 9
     def ==(other)
11 10
       return false unless other.is_a?(RCM::Package)
... ...
@@ -19,12 +18,14 @@ module RCM
19 18
       @name = name
20 19
       @version = version
21 20
       @state = state
21
+      @changed = false
22 22
     end
23 23
 
24 24
     def to_s
25 25
       "Name = #{@name}\n" +
26 26
       "Version = #{@version}\n" +
27
-      "State = #{@state}\n"
27
+      "State = #{@state}\n" +
28
+      "Changed = #{changed}"
28 29
     end
29 30
   end
30 31
 end
... ...
@@ -13,8 +13,7 @@ module RCM
13 13
         return false unless other_depends.key?(dep_name)
14 14
 
15 15
         # Return false if any of the attributes does not match
16
-        unless  other_depends[dep_name].checksum == dep_object.checksum &&
17
-                other_depends[dep_name].mode == dep_object.mode &&
16
+        unless  other_depends[dep_name].mode == dep_object.mode &&
18 17
                 other_depends[dep_name].owner == dep_object.owner &&
19 18
                 other_depends[dep_name].group == dep_object.group &&
20 19
                 other_depends[dep_name].path == dep_object.path
... ...
@@ -41,7 +40,26 @@ module RCM
41 40
 
42 41
     def to_s
43 42
       "Name = #{@name}\n" +
44
-      "Depends = #{@depends}\n"
43
+      "Depends on Files = #{@depends_file.values.join("\n")}" +
44
+      "Depends on Packages = #{@depends_package.values.join("\n")}"
45
+    end
46
+
47
+    def dependencies_changed?
48
+      return false if @depends_file.empty? && @depends_package.empty?
49
+
50
+      # Check file dependencies only if there are some.
51
+      if @depends_file && !@depends_file.empty?
52
+        @depends_file.values.each do |f|
53
+          return true if f.changed
54
+        end
55
+      end
56
+
57
+      if @depends_package && !@depends_package.empty?
58
+        @depends_package.values.each do |p|
59
+          return true if p.changed
60
+        end
61
+      end
62
+
45 63
     end
46 64
   end
47 65
 end
... ...
@@ -89,25 +89,34 @@ module RCM
89 89
     package_dependencies = {}
90 90
     markup.each do |d|
91 91
       dependencies = d.fetch('dependencies', {})
92
-      dependencies.each do |dep|
93
-        case dep
92
+      dependencies.each do |values|
93
+        values.each do |dep_type, dep|
94
+          case dep_type
94 95
           when PACKAGES
95
-          raise "\n\nPlease ensure #{dep['name']} is managed by this program before adding it as a dependency.\n\n" unless pkgs.key?(dep['name'])
96
-          p = pkgs[dep['name']]
97
-          package_dependencies[p.name] = p
96
+            dep.each do |pkg_definition|
97
+              pkg_name = pkg_definition['name']
98
+              raise "\n\nPlease ensure #{pkg_name} is managed by this program before adding it as a dependency.\n\n" unless pkgs.key?(pkg_name)
99
+              p = pkgs[pkg_name]
100
+              package_dependencies[pkg_name] = p
101
+            end
98 102
           when FILES
99
-          raise "\n\nPlease ensure #{dep['path']} is managed by this program before adding it as a dependency.\n\n" unless files.key?(dep['path'])
103
+            dep.each do |file_defnition|
104
+              path = file_defnition['path']
105
+              raise "\n\nPlease ensure #{path} is managed by this program before adding it as a dependency.\n\n" unless files.key?(path)
100 106
 
101
-          f = files[dep['path']]
102
-          file_dependencies[f.path] = f
107
+              f = files[path]
108
+              file_dependencies[path] = f
109
+            end
110
+          end
103 111
         end
112
+
104 113
       end
105 114
       s = RCM::Service.new(d['name'], file_dependencies, package_dependencies)
106 115
       @@wanted[SERVICES][d['name']] = s
107 116
     end
108 117
   end
109 118
 
110
-  def whachuwant # or parse_config
119
+  def self.whachuwant # or parse_config
111 120
     # Consume config from YAML, and convert it to usable objects in @@wanted.
112 121
     config_file = RCM_ENV.empty? ? 'config.yaml' : "config_#{RCM_ENV}.yaml"
113 122
 
... ...
@@ -131,52 +140,85 @@ module RCM
131 140
 
132 141
   end
133 142
 
134
-  def whachugot # or get_current_state
143
+  def self.whachugot # or get_current_state
135 144
     @@got[PACKAGES] = @@pkg_mgr.get_current_state(@@wanted[PACKAGES])
136 145
     @@got[FILES] = @@file_mgr.get_current_state(@@wanted[FILES])
137 146
     # @@got[SERVICES] = @@svc_mgr.get_current_state(@@wanted[SERVICES])
138 147
   end
139 148
 
140
-  def crackalackin_packages
149
+  def self.converge_packages
141 150
     @@wanted[PACKAGES].each do |pkg_name, pkg_obj|
142 151
       current_pkg = @@got[PACKAGES][pkg_name]
143 152
 
144
-      next if current_pkg == pkg_obj
153
+      if current_pkg == pkg_obj
154
+        @@logger.info("#{pkg_name} is in expected state.")
155
+        next
156
+      end
145 157
 
146
-      # Set the version correctly
147
-      current_pkg.version = pkg_obj.version
148
-      @@crackalackin[PACKAGES].push(current_pkg)
158
+      @@logger.debug('Missing package:' + pkg_name)
159
+      if current_pkg.version != pkg_obj.version && current_pkg.state == ::RCM::Package::INSTALLED
160
+        # Wrong version is installed. Remove current version and install the correct one.
161
+        # Not the best way to go about it... but we are not implementing a legit solution.
162
+        @logger.debug("#{current_pkg.name}=#{current_pkg.version} is installed. Uninstalling first.")
163
+        @@pkg_mgr.remove(pkg_obj)
164
+        @@pkg_mgr.install(pkg_obj)
165
+      elsif current_pkg.state == ::RCM::Package::REMOVED
166
+        @@pkg_mgr.install(pkg_obj)
149 167
       end
150
-    @@logger.debug('Missing packages:' + @@crackalackin[PACKAGES].join("\n"))
151 168
     end
152 169
 
153
-  def crackalackin_files
170
+  end
171
+
172
+  def self.converge_files
154 173
     @@wanted[FILES].each do |path, file_obj|
155 174
       file_on_fs = @@got[FILES][path]
156 175
 
157
-      next if file_on_fs == file_obj
176
+      if file_on_fs == file_obj
177
+        @@logger.info("#{path} is in expected state.")
178
+        next
179
+      end
180
+
181
+      # try to do minimal changes by finding what is different
182
+      if file_on_fs.path.empty?
183
+        # for files, we're not doing explicit state maintenance like we are doing for packages
184
+        # If file is on disk, file obj will contain the path.
185
+        @@logger.info("#{path} is not present on disk.")
186
+        @@file_mgr.copy(file_obj)
187
+        @@file_mgr.apply_attributes(file_obj)
188
+      elsif file_on_fs.mode != file_obj.mode ||
189
+          file_on_fs.owner != file_obj.owner ||
190
+          file_on_fs.group != file_obj.group
191
+        @@logger.debug("Attributes on #{path} are not in expected state.")
192
+        @@file_mgr.apply_attributes(file_obj)
193
+      elsif !::File.cmp(file_obj.src_path, path)
194
+        @@logger.debug("File contents don't match, even though #{path} is on disk.")
195
+        @@file_mgr.remove(file_obj)
196
+        @@file_mgr.copy(file_obj)
197
+        @@file_mgr.apply_attributes(file_obj)
198
+      end
158 199
 
159 200
       # Set the version correctly
160 201
       @@crackalackin[FILES].push(file_obj)
161 202
     end
162
-    @@logger.debug('Missing files:' + @@crackalackin[FILES].join("\n"))
163
-    puts @@crackalackin[FILES]
164 203
   end
165 204
 
166
-  def cracka_stop_lackin()
167
-    puts 'not implemented'
205
+  def self.restart_services_if_needed
206
+    @@wanted[SERVICES].values.each do |svc_obj|
207
+      @@svc_manager.restart(svc_obj) if @@svc_mgr.dependencies_changed?(svc_obj)
208
+    end
168 209
   end
169 210
 
170 211
   def main
171
-    self.configure_logger
212
+    configure_logger
213
+    whachuwant
214
+    whachugot
215
+    converge_packages
216
+    converge_files
217
+    restart_services_if_needed
172 218
   end
173
-  module_function :whachuwant, :whachugot, :crackalackin_packages, :crackalackin_files,
174
-                  :cracka_stop_lackin, :main
175 219
 
220
+  module_function :main
176 221
 end
177 222
 
178 223
 RCM.main
179
-RCM.whachuwant
180
-RCM.whachugot
181
-RCM.crackalackin_packages
182
-RCM.crackalackin_files
224
+
183 225