Now detecting difference in current and wanted state.
Dev

Dev commited on 2018-06-16 00:55:00
Showing 9 changed files, with 269 additions and 69 deletions.

... ...
@@ -1,3 +1,7 @@
1
+- log:
2
+  level: info
3
+  device: STDOUT
4
+
1 5
 - packages:
2 6
   - name: apache2
3 7
     version: 2.4.7-1ubuntu4.20
... ...
@@ -2,19 +2,19 @@
2 2
   - name: apache2
3 3
     version: 2.4.18-2ubuntu3.8
4 4
     desired_state: installed
5
-  - name: php5
6
-    version: 5.5.9+dfsg-1ubuntu4.25
5
+  - name: php7.0
6
+    version: 7.0.30-0ubuntu0.16.04.1
7 7
     desired_state: installed
8
-  - name: libapache2-mod-php5
9
-    version: 5.5.9+dfsg-1ubuntu4.25
8
+  - name: libapache2-mod-php
9
+    version: 1:7.0+35ubuntu6.1
10 10
     desired_state: installed
11 11
 
12 12
 - files:
13 13
   - path: /tmp/hello_world.php
14
-    owner: apache2
15
-    group: apache2
16
-    mode: 0644
17
-    local_file: hello_world.php
14
+    owner: dev
15
+    group: dev
16
+    mode: '0664'
17
+    local_file: /home/dev/src/rcm/resources/hello_world.php
18 18
     desired_state: present
19 19
 
20 20
 - services:
... ...
@@ -1,2 +1,71 @@
1
+require_relative '../objects/rcm_file'
2
+require_relative '../rcm_utils'
3
+require 'fileutils'
4
+
1 5
 module RCM
6
+  class FileManager
7
+    def get_id_to_name_mapping(file)
8
+      id_to_name = {}
9
+      ::File.open(file, 'r').each do |l|
10
+        matches = /(?<name>\w+):.+:(?<id>\d+):.*\s/.match(l)
11
+        id_to_name[matches[:id]] = matches[:name]
2 12
       end
13
+      id_to_name
14
+    end
15
+
16
+    def apply_attributes(file)
17
+      raise 'apply_attributes only works with ::RCM::File' unless file.is_a?(::RCM::File)
18
+
19
+      # Will throw exception if something poopy happens
20
+      ::FileUtils.chown(file.owner, file.group, file.path)
21
+      ::FileUtils.chmod(file.mode, file.path)
22
+
23
+      file.changed = true
24
+      file
25
+    end
26
+
27
+    def copy(file)
28
+      dir = ::File.dirname(file.path)
29
+      ::FileUtils.mkdir_p(dir)
30
+      ::FileUtils.copy(file.src_path, file.path, remove_destination: true)
31
+
32
+      file.changed = true
33
+      file
34
+    end
35
+
36
+    def remove(file)
37
+      file_existed = ::File.file?(file.path)
38
+      ::FileUtils.rm(file.path)
39
+      file.changed = true if file_existed
40
+      file
41
+    end
42
+
43
+    def initialize(logger)
44
+      @logger = logger
45
+    end
46
+
47
+    def get_current_state(wanted_files)
48
+      got = {}
49
+      uids = get_id_to_name_mapping('/etc/passwd')
50
+      gids = get_id_to_name_mapping('/etc/group')
51
+      wanted_files.each do |path, p|
52
+        f = ::RCM::File.new('', '', '', '', '')
53
+        # Put in an empty object if file does not exist
54
+        unless ::File.exist?(path)
55
+          got[path] = f
56
+          next
57
+        end
58
+
59
+        f_stat = ::File.stat(path)
60
+        f.path = path
61
+        f.owner = uids[f_stat.uid.to_s]
62
+        f.group = gids[f_stat.gid.to_s]
63
+        f.mode = f_stat.mode.to_s(8)[-4, 4]
64
+        got[path] = f
65
+      end
66
+
67
+      got
68
+    end
69
+  end
70
+end
71
+
... ...
@@ -5,10 +5,22 @@ module RCM
5 5
     APT_GET = '/usr/bin/apt-get'.freeze
6 6
     DPKG_QUERY = '/usr/bin/dpkg-query'.freeze
7 7
     APT_CACHE = '/usr/bin/apt-cache'.freeze
8
+    APT_PARTIAL_DIR = '/var/lib/apt/lists/partial'.freeze
9
+
10
+    def initialize(logger)
11
+      @logger = logger
12
+    end
8 13
 
9 14
     def update()
15
+      @logger.info('Updating apt cache.')
16
+      unless ::File.readable?(APT_PARTIAL_DIR)
17
+        @logger.warn('Skipping apt cache update as we are not running with right privilege.')
18
+        return
19
+      end
10 20
       status = ::RCM.cmd("#{APT_GET} update")
11
-      raise "Updating apt cache failed. \n\n#{status[:error]}\n\nstdout:\n#{status[:output]}"
21
+      unless status[:exit_code] == 0
22
+        @logger.warn("Updating apt cache failed. \n\n#{status[:error]}\n\nstdout:\n#{status[:output]}")
23
+      end
12 24
     end
13 25
 
14 26
     def status(pkg_name)
... ...
@@ -16,6 +28,8 @@ module RCM
16 28
     end
17 29
 
18 30
     def install(pkg_name, version = '')
31
+      raise "\n\nPlease ensure you are running with root privileges" unless ::File.readable?(APT_PARTIAL_DIR)
32
+
19 33
       name = ''
20 34
       version_string = "=#{version}" unless version.empty?
21 35
       if pkg_name.is_a?(String)
... ...
@@ -31,6 +45,8 @@ module RCM
31 45
     end
32 46
 
33 47
     def multi_install(pkgs)
48
+      raise "\n\nPlease ensure you are running with root privileges\n\n" unless ::File.readable?(APT_PARTIAL_DIR)
49
+
34 50
       raise 'multi_install expects array of ::RCM.Package' unless pkgs.is_a?(Array)
35 51
 
36 52
       # Construct string of packages to install in one go.
... ...
@@ -60,10 +76,33 @@ module RCM
60 76
     end
61 77
 
62 78
     def remove(pkg_name)
79
+      raise "\n\nPlease ensure you are running with root privileges\n\n" unless ::File.readable?(APT_PARTIAL_DIR)
80
+
63 81
       # Remove is idempotent and returns with success even if package is not installed.
64 82
       status = ::RCM.cmd("#{APT_GET} remove --yes #{pkg_name}")
65 83
       error_message = "Removing #{pkg_name} failed!\n\nstderr:\n#{status[:error]}\n\nstdout:\n#{status[:output]}"
66 84
       raise error_message unless status[:exit_code] == 0
67 85
     end
86
+
87
+    def get_current_state(wanted_packages)
88
+      got = {}
89
+      wanted_packages.each do |name, p|
90
+        state = ::RCM::Package::REMOVED
91
+        version = 'idk'
92
+        status = status(p.name)
93
+
94
+        if status[:exit_code] == 0
95
+          state = ::RCM::Package::INSTALLED if status[:output] =~ 'ok installed'
96
+          version = /Version: (?<version>\d+.+)/.match(status[:output])[:version]
97
+        end
98
+
99
+        current_pkg = ::RCM::Package.new(name, version, state)
100
+
101
+        got[name] = current_pkg
102
+      end
103
+
104
+      got
105
+
106
+    end
68 107
   end
69 108
 end
... ...
@@ -1,2 +1,13 @@
1
+require 'logger'
2
+
1 3
 module RCM
4
+  class ServiceManager
5
+    def initialize(logger)
6
+      @logger = logger
7
+    end
8
+
9
+    def self.dependencies_changed?(service)
10
+      # service.depn
11
+    end
12
+  end
2 13
 end
... ...
@@ -1,23 +1,28 @@
1
+require 'fileutils'
2
+
1 3
 module RCM
2 4
   class File
3
-    attr_accessor :path, :owner, :group, :mode, :checksum
5
+    attr_accessor :path, :owner, :group, :mode, :src_path, :changed
4 6
 
5 7
     def ==(other)
6 8
       return false unless other.is_a?(RCM::File)
7 9
 
8
-      @path == other.path &&
10
+      return false if @path.empty? || other.path.empty?
11
+
12
+      ::FileUtils.compare_file(@path, other.path) &&
9 13
           @owner == other.owner &&
10 14
           @group == other.group &&
11
-          @mode == other.mode &&
12
-          @checksum == other.checksum
15
+          @mode == other.mode
16
+
13 17
     end
14 18
 
15
-    def initialize(path, owner, group, mode, checksum)
19
+    def initialize(path, owner, group, mode, src_path)
16 20
       @path = path
17 21
       @owner = owner
18 22
       @group = group
19 23
       @mode = mode
20
-      @checksum = checksum
24
+      @src_path = src_path
25
+      @changed = false
21 26
     end
22 27
 
23 28
     def to_s
... ...
@@ -25,7 +30,8 @@ module RCM
25 30
       "Owner = #{@owner}\n" +
26 31
       "Group = #{@group}\n" +
27 32
       "Mode = #{@mode}\n" +
28
-      "Checksum = #{@checksum}"
33
+      "Source Path = #{src_path}\n" +
34
+      "Changed = #{@changed}"
29 35
     end
30 36
   end
31 37
 end
... ...
@@ -5,6 +5,7 @@ module RCM
5 5
     # Public state constants
6 6
     INSTALLED = 'installed'.freeze
7 7
     REMOVED = 'removed'.freeze
8
+    CHANGED = 'changed'.freeze
8 9
 
9 10
     def ==(other)
10 11
       return false unless other.is_a?(RCM::Package)
... ...
@@ -1,28 +1,47 @@
1 1
 module RCM
2 2
   class Service
3
-    attr_accessor :name, :installed, :definition_path, :running, :enabled, :depends
3
+    attr_accessor :name, :depends_file, :depends_package
4 4
 
5 5
     def ==(other)
6 6
       return false unless other.is_a?(RCM::Service)
7 7
 
8
-      @name == other.name && @installed == other.installed && @definition_path == other.definition_path &&
9
-          @running == other.running && @enabled == other.enabled
8
+      return false unless @name == other.name
9
+
10
+      @depends_file.each do |dep_name, dep_object|
11
+        other_depends = other.depends_file
12
+        # Return false if dependencies are not same.
13
+        return false unless other_depends.key?(dep_name)
14
+
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 &&
18
+                other_depends[dep_name].owner == dep_object.owner &&
19
+                other_depends[dep_name].group == dep_object.group &&
20
+                other_depends[dep_name].path == dep_object.path
21
+          return false
22
+        end
23
+      end
24
+
25
+      @depends_package.each do |dep_name, dep_object|
26
+        other_depends = other.depends_package
27
+        unless  other_depends[dep_name].name == dep_object.name &&
28
+                other_depends[dep_name].version == dep_object.version
29
+          return false
30
+        end
31
+      end
32
+
33
+      true
10 34
     end
11 35
 
12
-    def initialize(name, installed, definition_path, running, enabled)
36
+    def initialize(name, depends_file, depends_package)
13 37
       @name = name
14
-      @installed = installed
15
-      @definition_path = definition_path
16
-      @running = running
17
-      # Enabled to run on startup (SystemD-esque definition)
18
-      @enabled = enabled
38
+      @depends_file = depends_file
39
+      @depends_package = depends_package
19 40
     end
20 41
 
21 42
     def to_s
22 43
       "Name = #{@name}\n" +
23
-          "Installed = #{@installed}\n" +
24
-          "Definition Path = #{@definition_path}\n" +
25
-          "Running = #{@running}\nStart at boot = #{@enabled}"
44
+      "Depends = #{@depends}\n"
26 45
     end
27 46
   end
28 47
 end
... ...
@@ -1,13 +1,21 @@
1
+#!/usr/bin/ruby
2
+
1 3
 require_relative 'objects/rcm_file'
2 4
 require_relative 'objects/rcm_package'
3 5
 require_relative 'objects/rcm_service'
4 6
 require_relative 'managers/rcm_package_manager'
7
+require_relative 'managers/rcm_file_manager'
8
+require_relative 'managers/rcm_service_manager'
9
+require_relative 'rcm_utils'
5 10
 require 'yaml'
11
+require 'logger'
6 12
 
7 13
 module RCM
8
-  PACKAGES = 'packages'
9
-  FILES = 'files'
10
-  SERVICES = 'services'
14
+  PACKAGES = 'packages'.freeze
15
+  FILES = 'files'.freeze
16
+  SERVICES = 'services'.freeze
17
+  VALID_LOG_LEVELS = %w(fatal error info debug).freeze
18
+  RCM_ENV = ENV.fetch('ENVIRONMENT', '')
11 19
 
12 20
   @@wanted = {
13 21
     PACKAGES => {},
... ...
@@ -27,9 +35,23 @@ module RCM
27 35
     SERVICES => []
28 36
   }
29 37
 
30
-  @@pkg_mgr = ::RCM::Apt.new()
38
+  @@logger = ::Logger.new(STDOUT)
39
+
40
+  @@pkg_mgr = ::RCM::Apt.new(@@logger)
41
+  @@file_mgr = ::RCM::FileManager.new(@@logger)
42
+  @@svc_mgr = ::RCM::ServiceManager.new(@@logger)
43
+
44
+  def self.configure_logger
45
+    # Can be exposed as settings but was expanding the scope as this
46
+    # object will need to be passed to all other objects
47
+    @@logger.level = ::Logger::DEBUG
48
+    @@logger.progname = 'rcm'
49
+  end
31 50
 
32 51
   def self.parse_packages(markup)
52
+    @@logger.info("Environment = #{RCM_ENV}")
53
+    @@pkg_mgr.update unless RCM_ENV == 'dev'
54
+
33 55
     markup.each do |d|
34 56
       # ensure package version exists, if mentioned.
35 57
       version = d.fetch('version', '')
... ...
@@ -52,80 +74,109 @@ module RCM
52 74
     end
53 75
   end
54 76
 
77
+  def self.parse_files(markup)
78
+    markup.each do |d|
79
+      raise "\n\n'#{d['local_file']}' is not readable.\n\n" unless ::File.readable?(d['local_file'])
80
+      f = RCM::File.new(d['path'], d['owner'], d['group'], d['mode'], d['local_file'])
81
+      @@wanted[FILES][d['path']] = f
82
+    end
83
+  end
84
+
85
+  def self.parse_services(markup)
86
+    pkgs = @@wanted[PACKAGES]
87
+    files = @@wanted[FILES]
88
+    file_dependencies = {}
89
+    package_dependencies = {}
90
+    markup.each do |d|
91
+      dependencies = d.fetch('dependencies', {})
92
+      dependencies.each do |dep|
93
+        case dep
94
+        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
98
+        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'])
100
+
101
+          f = files[dep['path']]
102
+          file_dependencies[f.path] = f
103
+        end
104
+      end
105
+      s = RCM::Service.new(d['name'], file_dependencies, package_dependencies)
106
+      @@wanted[SERVICES][d['name']] = s
107
+    end
108
+  end
55 109
 
56
-  def whachuwant() # or parse_config
57
-    env = ENV.fetch('ENVIRONMENT', '')
110
+  def whachuwant # or parse_config
58 111
     # Consume config from YAML, and convert it to usable objects in @@wanted.
59
-    config_file = env.empty? ? 'config.yaml' : "config_#{env}.yaml"
112
+    config_file = RCM_ENV.empty? ? 'config.yaml' : "config_#{RCM_ENV}.yaml"
60 113
 
61 114
     raise "#{config_file} not found." unless ::File.readable?(config_file)
62 115
 
63 116
     config = YAML.load_file(config_file)
64 117
     config.each do |yaml_objects|
65
-      yaml_objects.each do |coll, defs|
66
-        case coll
118
+      yaml_objects.each do |collection, resource_definition|
119
+        case collection
67 120
         when PACKAGES
68
-          parse_packages(defs)
121
+          parse_packages(resource_definition)
69 122
 
70 123
         when FILES
71
-          defs.each do |d|
72
-            f = RCM::File.new(d['path'], d['owner'], d['group'], d['mode'], 'idk')
73
-            @@wanted[FILES][d['path']] = f
74
-          end
124
+          parse_files(resource_definition)
75 125
 
76 126
         when SERVICES
77
-          defs.each do |d|
78
-            puts d
79
-          end
127
+          parse_services(resource_definition)
80 128
         end
81 129
       end
82 130
     end
83 131
 
84 132
   end
85 133
 
86
-  def whachugot() # or get_current_state
87
-    @@wanted[PACKAGES].each do |name, p|
88
-      state = ::RCM::Package::REMOVED
89
-      version = 'idk'
90
-      begin
91
-        status = @@pkg_mgr.status(p.name)
92
-
93
-        if status[:exit_code] == 0
94
-          state = ::RCM::Package::INSTALLED if status[:output] =~ 'ok installed'
95
-          version = /Version: (?<version>\d+.+)/.match(status[:output])[:version]
134
+  def whachugot # or get_current_state
135
+    @@got[PACKAGES] = @@pkg_mgr.get_current_state(@@wanted[PACKAGES])
136
+    @@got[FILES] = @@file_mgr.get_current_state(@@wanted[FILES])
137
+    # @@got[SERVICES] = @@svc_mgr.get_current_state(@@wanted[SERVICES])
96 138
   end
97 139
 
98
-        current_pkg = ::RCM::Package.new(name, version, state)
99
-
100
-        @@got[PACKAGES][name] = current_pkg
101
-      rescue Exception => e
102
-        puts e.message
103
-      end
104
-    end
105
-  end
106
-
107
-  def crackalackin_packages()
140
+  def crackalackin_packages
108 141
     @@wanted[PACKAGES].each do |pkg_name, pkg_obj|
109 142
       current_pkg = @@got[PACKAGES][pkg_name]
110 143
 
111
-      next if current_pkg.state == ::RCM::Package::INSTALLED &&
112
-          current_pkg.version == pkg_obj.version
144
+      next if current_pkg == pkg_obj
113 145
 
114 146
       # Set the version correctly
115 147
       current_pkg.version = pkg_obj.version
116 148
       @@crackalackin[PACKAGES].push(current_pkg)
117 149
     end
150
+    @@logger.debug('Missing packages:' + @@crackalackin[PACKAGES].join("\n"))
151
+  end
152
+
153
+  def crackalackin_files
154
+    @@wanted[FILES].each do |path, file_obj|
155
+      file_on_fs = @@got[FILES][path]
156
+
157
+      next if file_on_fs == file_obj
118 158
 
119
-    puts @@crackalackin[PACKAGES]
159
+      # Set the version correctly
160
+      @@crackalackin[FILES].push(file_obj)
161
+    end
162
+    @@logger.debug('Missing files:' + @@crackalackin[FILES].join("\n"))
163
+    puts @@crackalackin[FILES]
120 164
   end
121 165
 
122 166
   def cracka_stop_lackin()
123 167
     puts 'not implemented'
124 168
   end
125
-  module_function :whachuwant, :whachugot, :crackalackin_packages, :cracka_stop_lackin
169
+
170
+  def main
171
+    self.configure_logger
172
+  end
173
+  module_function :whachuwant, :whachugot, :crackalackin_packages, :crackalackin_files,
174
+                  :cracka_stop_lackin, :main
126 175
 
127 176
 end
128 177
 
178
+RCM.main
129 179
 RCM.whachuwant
130 180
 RCM.whachugot
131 181
 RCM.crackalackin_packages
182
+RCM.crackalackin_files
132 183