ucmt 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2e407946f8dd4efc496b4232ae162ba9dc1bedb0bb9e530a0ba50d216d33f019
4
+ data.tar.gz: fb13b4a84d0452f2458bda86a950f4cd674e76e83662eaa4fdeed3aefbd912d9
5
+ SHA512:
6
+ metadata.gz: db9af1c0b72d1154028c1de3789aeae0f3b940754ca5c2285cf34b115fa3f511c2c9472ca75ccd5a8daf3be466d0762d286f6ba387fade942f7a45258c35e653
7
+ data.tar.gz: 0c7dec652069964770fd42b06e3b65a240642a905d308370da4809efa68dd5e7907daa4135607cfb96aa10fc89e5313a78e47b14065a6e6925c23896a671255a
data/bin/ucmt ADDED
@@ -0,0 +1,36 @@
1
+ #! /usr/bin/ruby
2
+
3
+ require 'optimist'
4
+ require "cheetah"
5
+
6
+ bin_dirs = ENV["PATH"].split(":")
7
+ commands = bin_dirs.each_with_object([]) do |dir, res|
8
+ Dir["#{dir}/ucmt-*"].each do |cmd|
9
+ res << File.basename(cmd).delete_prefix("ucmt-")
10
+ end
11
+ end
12
+
13
+ commands.uniq!
14
+ commands.sort!
15
+
16
+ opts = Optimist.options do
17
+ opt :list_commands, "List all available commands"
18
+ stop_on commands
19
+ end
20
+
21
+ if opts[:list_commands]
22
+ puts "Avaible commands:"
23
+ commands.each do |cmd|
24
+ help = Cheetah.run("ucmt-#{cmd}", "--help", stdout: :capture).lines.first.chomp
25
+ puts "#{cmd}\t#{help}"
26
+ end
27
+
28
+ exit 0
29
+ end
30
+
31
+ Optimist::educate if ARGV.empty?
32
+ Optimist::die "Unknown command '#{ARGV.first}'" unless commands.include?(ARGV.first)
33
+
34
+ cmd = ARGV.shift
35
+ ARGV.unshift("ucmt-#{cmd}")
36
+ exec(*ARGV)
data/bin/ucmt-ansible ADDED
@@ -0,0 +1,38 @@
1
+ #! /usr/bin/ruby
2
+
3
+ require "yaml"
4
+
5
+ require "ucmt/ansible"
6
+ require 'optimist'
7
+ opts = Optimist::options do
8
+ banner "Ansible backend to write salt configuration from UCMT configuration, run it or dry run it."
9
+ opt :config, "Create salt configuration from ucmt configuration. Can be passed also as STDIN.", type: String
10
+ opt :ansible_directory, "Where to write respective read states.yml", type: String, default: "~/"
11
+ opt :dry_run, "Just prints state and what actions will be applied"
12
+ opt :apply, "Apply configuration to system"
13
+ end
14
+ Optimist::die :apply, "Cannot have apply and dry_run together" if opts[:dry_run] && opts[:apply]
15
+
16
+ if opts[:config]
17
+ data = YAML.load_file(opts[:config])
18
+ else
19
+ stdin = STDIN.tty? ? nil : STDIN.read
20
+ data = YAML.load(stdin) if stdin && !stdin.empty?
21
+ end
22
+
23
+ ansible = UCMT::Ansible.new(File.expand_path(opts[:ansible_directory]))
24
+ nocmd = true
25
+ if data
26
+ ansible.write(data)
27
+ nocmd = false
28
+ end
29
+ if opts[:dry_run]
30
+ ansible.dry_run
31
+ nocmd = false
32
+ end
33
+ if opts[:apply]
34
+ ansible.apply
35
+ nocmd = false
36
+ end
37
+
38
+ Optimist::die "At least one of UCMT configuration/apply/dry-run has to be specified." if nocmd
@@ -0,0 +1,22 @@
1
+ #! /usr/bin/ruby
2
+
3
+ require "yaml"
4
+ require 'optimist'
5
+
6
+ require "ucmt/discovery/local_users"
7
+
8
+ def write(io, data)
9
+ io.puts(data.to_yaml)
10
+ end
11
+
12
+ opts = Optimist::options do
13
+ banner "Creates UCMT configuration from system configuration.\nCreated configuration contains more information when run as root user."
14
+ opt :path, "Path to UCMT configuration", type: String
15
+ end
16
+
17
+ users = UCMT::Discovery::LocalUsers.new.read_data
18
+ if opts[:path]
19
+ File.open(opts[:path], "w") { |f| write(f, users) }
20
+ else
21
+ write(STDOUT, users)
22
+ end
data/bin/ucmt-salt ADDED
@@ -0,0 +1,39 @@
1
+ #! /usr/bin/ruby
2
+
3
+ require "yaml"
4
+
5
+ require "ucmt/salt"
6
+ require 'optimist'
7
+
8
+ opts = Optimist::options do
9
+ banner "Salt backend to write salt configuration from UCMT configuration, run it or dry run it."
10
+ opt :config, "Create salt configuration from ucmt configuration. Can be passed also as STDIN.", type: String
11
+ opt :salt_directory, "Where to write respective read salt configuration", type: String, default: "/srv/salt"
12
+ opt :dry_run, "Just prints state and what actions will be applied"
13
+ opt :apply, "Apply configuration to system"
14
+ end
15
+ Optimist::die :apply, "Cannot have apply and dry_run together" if opts[:dry_run] && opts[:apply]
16
+
17
+ if opts[:config]
18
+ data = YAML.load_file(opts[:config])
19
+ else
20
+ stdin = STDIN.tty? ? nil : STDIN.read
21
+ data = YAML.load(stdin) if stdin && !stdin.empty?
22
+ end
23
+
24
+ salt = UCMT::Salt.new(opts[:salt_directory])
25
+ nocmd = true
26
+ if data
27
+ salt.write(data)
28
+ nocmd = false
29
+ end
30
+ if opts[:dry_run]
31
+ salt.dry_run
32
+ nocmd = false
33
+ end
34
+ if opts[:apply]
35
+ salt.apply
36
+ nocmd = false
37
+ end
38
+
39
+ Optimist::die "At least one of UCMT configuration/apply/dry-run has to be specified." if nocmd
data/bin/ucmt-users ADDED
@@ -0,0 +1,109 @@
1
+ #! /usr/bin/ruby
2
+
3
+ require "yaml"
4
+ require 'optimist'
5
+
6
+ require 'ucmt/users'
7
+
8
+ def write(io, data)
9
+ io.puts(data.to_yaml)
10
+ end
11
+
12
+ subcommands = [ "add", "edit", "remove", "ignore", "list", "show"]
13
+
14
+ user_opts = Optimist::options do
15
+ banner("CLI for user managent in UCMT configuration.\n" \
16
+ "Configuration can be passed on stdin and then modified one to stdout or edit in place when using config option.\n" \
17
+ "Usage:\n\n" \
18
+ " ucmt-users <users options> <action> <action options>\n\n" \
19
+ "Available actions: add, edit, remove, ignore, list, show\n" \
20
+ "Use ucmt-users <action> --help for more details for specific actions.\n" \
21
+ "Users options:\n")
22
+ opt :config, "Configuration to edit in place.", type: String
23
+ stop_on subcommands
24
+ end
25
+
26
+ if user_opts[:config]
27
+ data = File.exist?(user_opts[:config]) && YAML.load_file(user_opts[:config])
28
+ else
29
+ stdin = STDIN.tty? ? nil : STDIN.read
30
+ data = YAML.load(stdin) if stdin && !stdin.empty?
31
+ end
32
+ data ||= {}
33
+
34
+ users = UCMT::Users.new(data)
35
+
36
+ write_result = false
37
+
38
+ loop do
39
+ command = ARGV.shift
40
+ case command
41
+ when "add", "edit"
42
+ write_result = true
43
+ cmd_opts = Optimist::options do
44
+ banner "Adds/Edits user"
45
+ opt :name, "Name of user. Mandatory argument.", type: String
46
+ opt :fullname, "Full name of user.", type: String
47
+ opt :no_fullname, "Do not specify full name of user."
48
+ opt :uid, "User ID number.", type: Integer
49
+ opt :no_uid, "Do not specify User ID."
50
+ opt :primary_group, "User primary group specified by name.", type: String
51
+ opt :no_primary_group, "Do not specify primary group."
52
+ opt :shell, "User shell.", type: String
53
+ opt :no_shell, "Do not specify user shell."
54
+ opt :home, "User home directory.", type: String
55
+ opt :no_home, "Do not specify user home directory."
56
+ # TODO: password support
57
+ # opt :password, "Set user password. Both already encrypted and plain password is accepted. Always stored as encrypted.", type: String
58
+ # opt :no_password, "Do not specify user password."
59
+ # opt :forbid_logging, "Do not allow user to login"
60
+ stop_on subcommands
61
+ end
62
+ Optimist::die :name, "Name is mandatory" unless cmd_opts[:name]
63
+ users.edit(cmd_opts)
64
+ when "remove"
65
+ write_result = true
66
+ cmd_opts = Optimist::options do
67
+ banner "Marks user to be removed."
68
+ opt :name, "Name of user. Mandatory argument.", type: String
69
+ stop_on subcommands
70
+ end
71
+ Optimist::die :name, "Name is mandatory" unless cmd_opts[:name]
72
+ users.remove(cmd_opts[:name])
73
+ when "ignore"
74
+ write_result = true
75
+ cmd_opts = Optimist::options do
76
+ banner "Mark user to not be modified."
77
+ opt :name, "Name of user. Mandatory argument.", type: String
78
+ stop_on subcommands
79
+ end
80
+ Optimist::die :name, "Name is mandatory" unless cmd_opts[:name]
81
+ users.ignore(cmd_opts[:name])
82
+ when "list"
83
+ cmd_opts = Optimist::options do
84
+ banner "List all user to modify."
85
+ stop_on subcommands
86
+ end
87
+ users.list
88
+ when "show"
89
+ cmd_opts = Optimist::options do
90
+ banner "Mark user to not be modified."
91
+ opt :name, "Name of user. Mandatory argument.", type: String
92
+ stop_on subcommands
93
+ end
94
+ Optimist::die :name, "Name is mandatory" unless cmd_opts[:name]
95
+ users.show(cmd_opts[:name])
96
+ when nil
97
+ break
98
+ else
99
+ Optimist.die "Invalid action '#{command}'"
100
+ end
101
+ end
102
+
103
+ if write_result
104
+ if user_opts[:config]
105
+ File.open(user_opts[:config], "w") { |f| write(f, data) }
106
+ else
107
+ write(STDOUT, data)
108
+ end
109
+ end
@@ -0,0 +1,76 @@
1
+ require "yaml"
2
+ require "cheetah"
3
+ require "fileutils"
4
+
5
+ module UCMT
6
+ class Ansible
7
+ def initialize(output_dir)
8
+ @output_dir = output_dir
9
+ end
10
+
11
+ def write(data)
12
+ FileUtils.mkdir_p(@output_dir)
13
+
14
+ result = local_users_content(data)
15
+
16
+ content = [{
17
+ "name" => "UCMT defined tasks",
18
+ "hosts" => "localhost",
19
+ "connection" => "local",
20
+ "tasks" => result
21
+ }]
22
+
23
+ File.write(File.join(@output_dir, "states.yml"), content.to_yaml)
24
+ end
25
+
26
+ def dry_run
27
+ Cheetah.run("ansible-playbook", File.join(@output_dir, "states.yml"), "--check", "--diff", stdout: STDOUT)
28
+ end
29
+
30
+ def apply
31
+ Cheetah.run("ansible-playbook", File.join(@output_dir, "states.yml"), stdout: STDOUT)
32
+ end
33
+
34
+ private
35
+
36
+ USERS_MAPPING = {
37
+ "fullname" => "comment",
38
+ "name" => "name",
39
+ "uid" => "uid",
40
+ "groups" => "groups",
41
+ "primary_group" => "group",
42
+ "shell" => "shell",
43
+ "home" => "home",
44
+ "password" => "password"
45
+ }
46
+ def local_users_content(data)
47
+ result = []
48
+
49
+ users_data = data["local_users"]
50
+ return [] unless users_data
51
+
52
+ (users_data["add"] || []).each do |user|
53
+ res = { "name" => "User #{user["name"]}" }
54
+ key2 = "ansible.builtin.user"
55
+ res[key2] = {}
56
+ USERS_MAPPING.each_pair do |k, v|
57
+ res[key2][v] = user[k] if user[k]
58
+ end
59
+
60
+ result << res
61
+ end
62
+
63
+ (users_data["remove"] || []).each do |user|
64
+ res = { "name" => "remove " + user["name"] }
65
+ key2 = "ansible.builtin.user"
66
+ res[key2] = { "name" => user["name"],
67
+ "state" => "absent", "remove" => "yes" }
68
+
69
+ result << res
70
+ end
71
+
72
+ result
73
+ end
74
+ end
75
+ end
76
+
@@ -0,0 +1,96 @@
1
+ require "json"
2
+ require "cheetah"
3
+
4
+ module UCMT
5
+ module Discovery
6
+ class LocalUsers
7
+ # TODO: remote machine
8
+ def initialize
9
+ end
10
+
11
+ def read_data
12
+ {
13
+ "local_users" => {
14
+ "add" => read
15
+ }
16
+ }
17
+ end
18
+
19
+ private
20
+
21
+ def read_users
22
+ output = Cheetah.run("ansible", "localhost", "-m", "getent", "-a", "database=passwd", stdout: :capture)
23
+
24
+ res = JSON.parse(output.sub(/^.*=>/, ""))
25
+ res["ansible_facts"]["getent_passwd"]
26
+ end
27
+
28
+ def read_groups
29
+ output = Cheetah.run("ansible", "localhost", "-m", "getent", "-a", "database=group", stdout: :capture)
30
+
31
+ res = JSON.parse(output.sub(/^.*=>/, ""))
32
+ res["ansible_facts"]["getent_group"]
33
+ end
34
+
35
+ def read_passwords
36
+ output = Cheetah.run("ansible", "localhost", "-m", "getent", "-a", "database=shadow", stdout: :capture)
37
+
38
+ res = JSON.parse(output.sub(/^.*=>/, ""))
39
+ res["ansible_facts"]["getent_shadow"]
40
+ end
41
+
42
+ USERS_KEYS_MAPPING = {
43
+ "uid" => 1,
44
+ "gid" => 2,
45
+ "fullname" => 3,
46
+ "home" => 4,
47
+ "shell" => 5
48
+ }
49
+ INTEGER_KEYS = ["uid", "gid"]
50
+ SYSTEM_USER_LIMIT = 500
51
+
52
+ GROUPS_KEYS_MAPPING = {
53
+ "gid" => 1,
54
+ "users" => 2
55
+ }
56
+
57
+ def read
58
+ # reading shadow need root permissions
59
+ # TODO: for remote check needs to be different
60
+ if Process.euid == 0
61
+ passwd = read_passwords
62
+ else
63
+ passwd = {}
64
+ end
65
+
66
+ groups = read_groups
67
+
68
+ users = read_users.map do |dk, dv|
69
+ USERS_KEYS_MAPPING.each_with_object({"name" => dk}) { |(k, v), r| r[k] = dv[v] }
70
+ end
71
+ users.each { |u| INTEGER_KEYS.each { |i| u[i] = u[i].to_i } }
72
+ users.select! { |v| v["uid"] == 0 || v["uid"] > SYSTEM_USER_LIMIT } # select only non system users
73
+ users.each do |user|
74
+ user["groups"] = []
75
+
76
+ groups.each do |name, group|
77
+ gid = group[GROUPS_KEYS_MAPPING["gid"]].to_i
78
+ group_users = group[GROUPS_KEYS_MAPPING["users"]].split(",") # see man group
79
+
80
+ if user["gid"] == gid || group_users.include?(user["name"])
81
+ user["groups"] << name
82
+ end
83
+
84
+ if user["gid"] == gid
85
+ user["primary_group"] = name
86
+ user.delete("gid")
87
+ end
88
+ end
89
+ user["password"] = passwd[user["name"]].first if passwd[user["name"]]
90
+ end
91
+
92
+ users
93
+ end
94
+ end
95
+ end
96
+ end
data/lib/ucmt/salt.rb ADDED
@@ -0,0 +1,73 @@
1
+ require "yaml"
2
+ require "cheetah"
3
+ require "fileutils"
4
+
5
+ module UCMT
6
+ class Salt
7
+ def initialize(output_dir)
8
+ @output_dir = output_dir
9
+ end
10
+
11
+ def write(data)
12
+ FileUtils.mkdir_p(@output_dir)
13
+
14
+ states = []
15
+ states << "local_users" if write_local_users(data)
16
+
17
+ write_states(states)
18
+ end
19
+
20
+ def dry_run
21
+ Cheetah.run("salt-call", "--local", "--file-root=#{@output_dir}", "state.apply", "test=true", stdout: STDOUT)
22
+ end
23
+
24
+ def apply
25
+ Cheetah.run("salt-call", "--local", "--file-root=#{@output_dir}", "state.apply", stdout: STDOUT)
26
+ end
27
+
28
+ private
29
+
30
+ def write_states(states)
31
+ content = { "base" => { "*" => states } }
32
+
33
+ File.write(File.join(@output_dir, "top.sls"), content.to_yaml)
34
+ end
35
+
36
+ USERS_MAPPING = {
37
+ "fullname" => "fullname",
38
+ "name" => "name",
39
+ "uid" => "uid",
40
+ "groups" => "groups",
41
+ "primary_group" => "gid",
42
+ "shell" => "shell",
43
+ "home" => "home",
44
+ "password" => "password"
45
+ }
46
+ def write_local_users(data)
47
+ result = {}
48
+
49
+ users_data = data["local_users"]
50
+ return false unless users_data
51
+
52
+ (users_data["add"] || []).each do |user|
53
+ key = user["name"]
54
+ key2 = "user.present"
55
+ result[key] = { key2 => [] }
56
+ target = result[key][key2]
57
+ USERS_MAPPING.each_pair do |k, v|
58
+ target << { v => user[k] } if user[k]
59
+ end
60
+ end
61
+
62
+ (users_data["remove"] || []).each do |user|
63
+ key = "remove " + user["name"]
64
+ key2 = "user.absent"
65
+ result[key] = { key2 => [{"name" => user["name"]}] }
66
+ end
67
+
68
+ File.write(File.join(@output_dir, "local_users.sls"), result.to_yaml)
69
+
70
+ return true
71
+ end
72
+ end
73
+ end
data/lib/ucmt/users.rb ADDED
@@ -0,0 +1,89 @@
1
+ require "yaml"
2
+ require "cheetah"
3
+ require "fileutils"
4
+
5
+ module UCMT
6
+ class Users
7
+ attr_reader :data
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ end
12
+
13
+ def ignore(name)
14
+ (users["add"] || []).delete_if { |u| u["name"] == name }
15
+ (users["remove"] || []).delete_if { |u| u["name"] == name }
16
+ end
17
+
18
+ def remove(name)
19
+ (users["add"] || []).delete_if { |u| u["name"] == name }
20
+ users["remove"] ||= []
21
+ users["remove"] << { "name" => name }
22
+ end
23
+
24
+ def edit(options)
25
+ name = options[:name]
26
+
27
+ # ensure that user is not removed
28
+ (users["remove"] || []).delete_if { |u| u["name"] == name }
29
+ users["add"] ||= []
30
+ user = users["add"].find { |u| u["name"] == name }
31
+ if !user
32
+ user = { "name" => name }
33
+ users["add"] << user
34
+ end
35
+
36
+ handle_option(:fullname, user, options)
37
+ handle_option(:uid, user, options)
38
+ handle_option(:primary_group, user, options)
39
+ handle_option(:shell, user, options)
40
+ handle_option(:home, user, options)
41
+
42
+ # TODO: PASSWORD
43
+ # TODO: groups
44
+ end
45
+
46
+ def list
47
+ puts "Users to add/edit:"
48
+ to_add = (users["add"] || []).map {|u| u["name"]}
49
+ puts to_add.to_yaml unless to_add.empty?
50
+ puts "\nUsers to remove:"
51
+ to_remove = (users["remove"] || []).map {|u| u["name"]}
52
+ puts to_remove.to_yaml unless to_remove.empty?
53
+ puts
54
+ end
55
+
56
+ def show(name)
57
+ remove = (users["remove"] || []).find { |u| u["name"] == name }
58
+ if remove
59
+ puts "User #{name} will be removed."
60
+ return
61
+ end
62
+ add = (users["add"] || []).find { |u| u["name"] == name }
63
+ if !add
64
+ puts "User #{name} won't be modified."
65
+ return
66
+ end
67
+
68
+ puts "User #{name} will have following settings:"
69
+ puts add.to_yaml
70
+ end
71
+
72
+ private
73
+
74
+ def handle_option(key, user, options)
75
+ if options[:"no_#{key}"]
76
+ user.delete(key.to_s)
77
+ elsif options[key]
78
+ user[key.to_s] = options[key]
79
+ end
80
+ end
81
+
82
+ def users
83
+ return @users if @users
84
+
85
+ @data["local_users"] ||= {}
86
+ @users = @data["local_users"]
87
+ end
88
+ end
89
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ucmt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Josef Reidinger
8
+ autorequire:
9
+ bindir: bin/
10
+ cert_chain: []
11
+ date: 2021-03-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: optimist
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: cheetah
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.5.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.5.2
41
+ description: A set of tools to generate configuration for various configuration management
42
+ tools like salt or ansible.
43
+ email: jreidinger@suse.com
44
+ executables:
45
+ - ucmt-discovery
46
+ - ucmt-ansible
47
+ - ucmt-salt
48
+ - ucmt
49
+ - ucmt-users
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - bin/ucmt
54
+ - bin/ucmt-ansible
55
+ - bin/ucmt-discovery
56
+ - bin/ucmt-salt
57
+ - bin/ucmt-users
58
+ - lib/ucmt/ansible.rb
59
+ - lib/ucmt/discovery/local_users.rb
60
+ - lib/ucmt/salt.rb
61
+ - lib/ucmt/users.rb
62
+ homepage: https://github.com/jreidinger/ucmt
63
+ licenses:
64
+ - GPL-2.0
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 2.7.6.2
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Universal configuration management tool
86
+ test_files: []