keyman 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- keyman (1.0.0)
4
+ keyman (1.1.0)
5
+ highline
5
6
  net-ssh (~> 2.6.3)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
11
+ highline (1.6.15)
10
12
  net-ssh (2.6.3)
11
13
 
12
14
  PLATFORMS
data/README.md CHANGED
@@ -4,10 +4,6 @@ This simple little utility allows you to manage the authorized_keys files for a
4
4
  number of servers & users. It is designed to provide easy access to ensure that
5
5
  you can revoke & grant access to appropriate people on multiple servers.
6
6
 
7
- **Please Note: this utility is somewhat un-tested and not currently used in any
8
- production environment. Your mileage may vary and we recommend testing in a
9
- non-production environment prior to use.**
10
-
11
7
  ## Installation
12
8
 
13
9
  To install, just install the Rubygem.
@@ -17,12 +13,19 @@ $ gem install keyman
17
13
  ```
18
14
 
19
15
  Once installed, you will need to create yourself a **manifest directory**. This
20
- directory will contain all your configuration for your key manager. You should
21
- create an empty directory and add two files, a `servers.rb` and a `users.rb` file.
16
+ directory will contain all your configuration for your key manager. You can easily
17
+ do this using the `init` command:
18
+
19
+ ```bash
20
+ $ keyman init path/to/manifest
21
+ ```
22
+
23
+ This will create a directory containing two files, a `users.km` and a `servers.km`.
24
+ These files contain examples and comments which should help you get started.
22
25
 
23
26
  ## Example Users/Groups Manifest File
24
27
 
25
- The below file is an example of a `users.rb` manifest file.
28
+ The below file is an example of a `users.km` manifest file.
26
29
 
27
30
  ```ruby
28
31
  group :admins do
@@ -39,7 +42,7 @@ end
39
42
 
40
43
  ## Example Server Manifest File
41
44
 
42
- The below file is an example of a `servers.rb` file.
45
+ The below file is an example of a `servers.km` file.
43
46
 
44
47
  ```ruby
45
48
  # An example configuration for a server where all admin users have
@@ -56,13 +59,30 @@ server do
56
59
  host 'database01.myapplication.com'
57
60
  user 'root', :admins, :dan
58
61
  end
62
+
63
+ # An example of a group of servers each with the same permissions. These
64
+ # will create servers with the same
65
+ server_group :load_balancers do
66
+ host 'lb01.myapplication.com'
67
+ host 'lb02.myapplication.com'
68
+ host 'lb03.myapplication.com'
69
+ user 'root', :admins
70
+ user 'app', :admins, :staff
71
+ end
59
72
  ```
60
73
 
74
+ You may add as many `.km` files as you wish to to your manifest directory and they
75
+ will be loaded. However, all **users** should be defined in `users.km` and nowhere
76
+ else.
77
+
61
78
  ## Pushing files to servers
62
79
 
63
- In order to push files to the server, you must already have YOUR key on the
64
- machine in order to authenticate. If you do not, you will not have access
65
- and will therefore be unable to push configuration.
80
+ In order to push your authorized_keys files to your servers, keyman must be able
81
+ to authenticate. In the first instance, we will attempt to use your local SSH keys
82
+ to do this. If we cannot authenticate with these, you will be prompted for the password
83
+ when you attempt to push. This password, if accepted, will then be cached for your
84
+ "session" and attempted for any subsequent servers which cannot be authenticated with
85
+ your SSH keys.
66
86
 
67
87
  ```bash
68
88
  $ cd path/to/manifest
@@ -70,6 +90,8 @@ $ cd path/to/manifest
70
90
  $ keyman push
71
91
  # to push configuration to a specific server
72
92
  $ keyman push database01.myapplication.com
93
+ # to push configuration to a server group
94
+ $ keyman push load_balancers
73
95
  ```
74
96
 
75
97
  There are other commands available within the app, you can view these by
@@ -78,3 +100,13 @@ viewing the inline help.
78
100
  ```bash
79
101
  $ keyman help
80
102
  ```
103
+
104
+ ## Storing your manifest directory
105
+
106
+ It is recommended to store your manifest directory in a Git repository. Once in a
107
+ repository, you will be required to ensure that your local branch is always the same
108
+ as your remote branch before you can push to a server. This ensures that you cannot
109
+ overwrite someone elses changes should you forget to pull before pushing.
110
+
111
+ This behaviour is automatic and currently non-optional when there is a .git directory
112
+ in your manifest.
data/bin/keyman CHANGED
@@ -22,17 +22,36 @@ begin
22
22
  puts "commands. In order to use this, you must be currently within your"
23
23
  puts "manifest directory."
24
24
  puts
25
- puts " keys {server} {user} - displays the authorized keys file for a server's user"
26
- puts " push - pushes the latest files to all servers"
27
- puts " push {server} - pushes the latest file to the specified server"
28
- puts " servers - displays a list of all servers"
29
- puts " permissions {server} - displays the permissions for given server"
30
- puts " users - displays a list of all users & groups"
25
+ puts " init {path} - creates a new manifest directory"
26
+ puts " keys {server} {user} - displays the authorized keys file for a server's user"
27
+ puts " push - pushes the latest files to all servers"
28
+ puts " push {server} - pushes the latest file to the specified server"
29
+ puts " servers - displays a list of all servers"
30
+ puts " permissions {server} - displays the permissions for given server"
31
+ puts " users - displays a list of all users & groups"
31
32
  puts
33
+ when 'init'
34
+ require 'fileutils'
35
+ if path = final_args[1]
36
+ if File.exist?(path)
37
+ raise Keyman::Error, "A file/directory already exists at #{path}"
38
+ else
39
+ FileUtils.mkdir(path)
40
+ template_root = File.expand_path(File.join('..', '..', 'templates'), __FILE__)
41
+ File.open(File.join(path, 'users.km'), 'w') { |f| f.write(File.read(File.join(template_root, 'users.km')))}
42
+ File.open(File.join(path, 'servers.km'), 'w') { |f| f.write(File.read(File.join(template_root, 'servers.km')))}
43
+ puts "\e[32mKeyman manifest directory created at #{path} successfully.\e[0m"
44
+ end
45
+ else
46
+ raise Keyman::Error, "You should pass a directory name to the init command to create a new manifest directory."
47
+ end
32
48
  else
49
+ puts "\e[37mUsing manifest from #{Keyman.manifest_dir}\e[0m"
33
50
  Keyman.run(final_args, options)
34
51
  end
35
52
 
53
+ rescue SystemExit, Interrupt
54
+ Process.exit(0)
36
55
  rescue Keyman::Error => e
37
56
  puts "\e[31m" + e.message + "\e[0m"
38
57
  Process.exit(1)
@@ -16,5 +16,6 @@ Gem::Specification.new do |s|
16
16
  s.email = "adam@atechmedia.com"
17
17
  s.homepage = "http://atechmedia.com"
18
18
  s.add_dependency('net-ssh', '~> 2.6.3')
19
+ s.add_dependency('highline')
19
20
  end
20
21
 
@@ -1,45 +1,55 @@
1
+ require 'yaml'
1
2
  require 'net/ssh'
3
+ require 'highline/import'
2
4
 
3
5
  require 'keyman/user'
4
6
  require 'keyman/group'
5
7
  require 'keyman/server'
6
- require 'keyman/keyfile'
7
-
8
+ require 'keyman/server_group'
9
+ require 'keyman/manifest'
8
10
 
9
11
  module Keyman
10
12
 
11
13
  # The current version of the keyman system
12
- VERSION = '1.0.0'
14
+ VERSION = '1.1.1'
13
15
 
14
16
  # An error which will be raised
15
17
  class Error < StandardError; end
16
18
 
17
19
  class << self
20
+ # Stores the actual manifest object
21
+ attr_accessor :manifest
22
+
23
+ # Storage for the manifest directory to work with
24
+ attr_accessor :manifest_dir
25
+
18
26
  # Storage for all the users, groups & servers which are loaded
19
27
  # from the manifest
20
28
  attr_accessor :users
21
29
  attr_accessor :groups
22
30
  attr_accessor :servers
23
-
24
- # Load a manifest from the given folder
25
- def load(directory)
26
- self.users = []
27
- self.groups = []
28
- self.servers = []
29
- if File.directory?(directory)
30
- ['groups.rb', 'servers.rb'].each do |file|
31
- path = File.join(directory, file)
32
- if File.exist?(path)
33
- Keyman::Keyfile.class_eval(File.read(path))
34
- else
35
- raise Error, "No '#{file}' was found in your manifest directory. Abandoning..."
36
- end
31
+ attr_accessor :server_groups
32
+
33
+ # Storage for a password cache to use within the current session
34
+ attr_accessor :password_cache
35
+
36
+ # Sets the default manifest_dir dir
37
+ def manifest_dir
38
+ @manifest_dir || self.config['manifest_dir'] || "./"
39
+ end
40
+
41
+ # Sets the configuration options
42
+ def config
43
+ @config ||= begin
44
+ config_dir = File.join(ENV['HOME'], '.keyman')
45
+ if File.exist?(config_dir)
46
+ YAML.load_file(config_dir)
47
+ else
48
+ {}
37
49
  end
38
- else
39
- raise Error, "No folder found at '#{directory}'"
40
50
  end
41
51
  end
42
-
52
+
43
53
  # Return a user or a group for the given name
44
54
  def user_or_group_for(name)
45
55
  self.users.select { |u| u.name == name.to_sym }.first || self.groups.select { |u| u.name == name.to_sym }.first
@@ -47,7 +57,7 @@ module Keyman
47
57
 
48
58
  # Execute a CLI command
49
59
  def run(args, options = {})
50
- load('./')
60
+ Manifest.load
51
61
  case args.first
52
62
  when 'keys'
53
63
  if server = self.servers.select { |s| s.host == args[1] }.first
@@ -73,10 +83,26 @@ module Keyman
73
83
  raise Error, "No server found with the hostname '#{args[1]}'"
74
84
  end
75
85
  when 'push'
86
+
87
+ if self.manifest.uses_git?
88
+ unless self.manifest.clean?
89
+ raise Error, "Your manifest is not clean. You should push to your repository before pushing."
90
+ end
91
+
92
+ unless self.manifest.latest_commit?
93
+ raise Error, "The remote server has a more up-to-date manifest. Pull first."
94
+ end
95
+
96
+ puts "\e[32mRepository check passed!\e[0m"
97
+ end
98
+
76
99
  if args[1]
77
- # push single server
78
- if server = self.servers.select { |s| s.host == args[1] }.first
100
+ server = self.servers.select { |s| s.host == args[1] }.first
101
+ server = self.server_groups.select { |s| s.name == args[1].to_sym }.first if server.nil?
102
+ if server.is_a?(Keyman::Server)
79
103
  server.push!
104
+ elsif server.is_a?(Keyman::ServerGroup)
105
+ server.servers.each(&:push!)
80
106
  else
81
107
  raise Error, "No server found with the hostname '#{args[1]}'"
82
108
  end
@@ -84,9 +110,21 @@ module Keyman
84
110
  self.servers.each(&:push!)
85
111
  end
86
112
  when 'servers'
87
- self.servers.each do |server|
88
- puts " * " + server.host
113
+ self.server_groups.sort_by(&:name).each do |group|
114
+ puts '-' * 80
115
+ puts group.name.to_s
116
+ puts '-' * 80
117
+ group.servers.each do |s|
118
+ puts " * #{s.host}"
119
+ end
120
+ end
121
+ puts '-' * 80
122
+ puts 'no group'
123
+ puts '-' * 80
124
+ self.servers.select { |s| s.group.nil?}.each do |s|
125
+ puts " * #{s.host}"
89
126
  end
127
+
90
128
  when 'users'
91
129
  self.groups.each do |group|
92
130
  puts "-" * 80
@@ -97,6 +135,27 @@ module Keyman
97
135
  puts "\e[37m#{u.key}\e[0m"
98
136
  end
99
137
  end
138
+
139
+ when 'status'
140
+ if Keyman.manifest.uses_git?
141
+ puts "Your manifest is using a remote git repository."
142
+ if Keyman.manifest.clean?
143
+ puts " * Your working copy is clean"
144
+ else
145
+ puts " * You have an un-clean working copy. You must commit before pushing."
146
+ end
147
+
148
+ if Keyman.manifest.latest_commit?
149
+ puts " * You have the latest commit fetched."
150
+ else
151
+ puts " * There is a newer version of this repo on the server."
152
+ end
153
+ else
154
+ puts "Your manifest does not use git. There is no status to display."
155
+ end
156
+
157
+ else
158
+ raise Error, "Invalid command '#{args.first}'"
100
159
  end
101
160
  end
102
161
  end
@@ -9,6 +9,9 @@ module Keyman
9
9
 
10
10
  # Add a new group with the given name
11
11
  def self.add(name, &block)
12
+ if existing = Keyman.user_or_group_for(name)
13
+ raise Error, "#{existing.class.to_s.split('::').last} already exists for '#{name}' - cannot define user with this name."
14
+ end
12
15
  g = Group.new
13
16
  g.name = name
14
17
  g.instance_eval(&block)
@@ -0,0 +1,77 @@
1
+ module Keyman
2
+ class Manifest
3
+
4
+ # Loads a manifest directory as live
5
+ def self.load(directory = Keyman.manifest_dir)
6
+ Keyman.users = []
7
+ Keyman.groups = []
8
+ Keyman.servers = []
9
+ Keyman.server_groups = []
10
+ manifest = self.new(directory)
11
+ if File.directory?(directory)
12
+ [File.join(directory, 'users.km'), Dir[File.join(directory, '*.km')]].flatten.uniq.compact.each do |file|
13
+ if File.exist?(file)
14
+ manifest.instance_eval(File.read(file))
15
+ else
16
+ raise Error, "No '#{file}' was found in your manifest directory. Abandoning..."
17
+ end
18
+ end
19
+ Keyman.manifest = manifest
20
+ else
21
+ raise Error, "No folder found at '#{directory}'"
22
+ end
23
+ end
24
+
25
+ # Initialize a new manifest with the current directory
26
+ def initialize(directory)
27
+ @directory = directory
28
+ end
29
+
30
+ # Adds a new group
31
+ def group(name, &users_block)
32
+ Group.add(name, &users_block)
33
+ end
34
+
35
+ # Adds a new server
36
+ def server(&block)
37
+ Server.add(&block)
38
+ end
39
+
40
+ # Adds a new user
41
+ def user(username, key)
42
+ User.add(username, key)
43
+ end
44
+
45
+ # Adds a new server group
46
+ def server_group(name, &block)
47
+ ServerGroup.add(name, &block)
48
+ end
49
+
50
+ # Does this manifest directory use git?
51
+ def uses_git?
52
+ File.directory?(File.join(@directory, '.git'))
53
+ end
54
+
55
+ # Is the current repo clean?
56
+ def clean?
57
+ `cd #{@directory} && git status`.chomp.include?('nothing to commit')
58
+ end
59
+
60
+ # Does the latest commit on the current branch match the remote brandh
61
+ def latest_commit?
62
+ local = `cd #{@directory} && git log --pretty=%H -n 1`.chomp
63
+ if `cd #{@directory} && git status`.chomp.match(/On branch (.*)\n/)
64
+ branch = $1
65
+ else
66
+ raise Error, "Unable to determine the local repository branch."
67
+ end
68
+ remote = `cd #{@directory} && git ls-remote 2> /dev/null | grep refs/heads/#{branch} `.chomp.split(/\s+/).first
69
+ if local.length == 40 && remote.length == 40
70
+ local == remote
71
+ else
72
+ raise Error, "Unable to determine local & remote commits"
73
+ end
74
+ end
75
+
76
+ end
77
+ end
@@ -1,7 +1,7 @@
1
1
  module Keyman
2
2
  class Server
3
3
 
4
- attr_accessor :host, :users, :location
4
+ attr_accessor :host, :users, :location, :group
5
5
 
6
6
  def initialize
7
7
  @users = {}
@@ -15,6 +15,17 @@ module Keyman
15
15
  s
16
16
  end
17
17
 
18
+ # Adds a new server based on it's name and location
19
+ def self.add_by_name(host, options = {})
20
+ s = self.new
21
+ s.host = host
22
+ s.location = options[:location]
23
+ s.users = options[:users]
24
+ s.group = options[:group]
25
+ Keyman.servers << s
26
+ s
27
+ end
28
+
18
29
  # Returns or sets the hostname of the server which should be used when connecting
19
30
  # and identifying this server
20
31
  def host(host = nil)
@@ -24,6 +35,7 @@ module Keyman
24
35
  # Sets a user on the server along with the access objects which should be
25
36
  # granted access
26
37
  def user(name, *access_objects)
38
+ access_objects.each { |ao| raise Error, "!! Invalid access object '#{ao}' on '#{self.host}'" unless Keyman.user_or_group_for(ao) }
27
39
  @users[name] = access_objects
28
40
  end
29
41
 
@@ -36,9 +48,12 @@ module Keyman
36
48
  # all objects from within the server
37
49
  def authorized_users(username)
38
50
  @users[username].map do |k|
39
- obj = Keyman.user_or_group_for(k)
40
- obj.is_a?(Group) ? obj.users : obj
41
- end.flatten.uniq
51
+ if obj = Keyman.user_or_group_for(k)
52
+ obj.is_a?(Group) ? obj.users : obj
53
+ else
54
+ nil
55
+ end
56
+ end.flatten.compact.uniq
42
57
  end
43
58
 
44
59
  # Returns a full string output for the authorized_keys file. Passes
@@ -59,20 +74,39 @@ module Keyman
59
74
  # configured here. This will not succeed if the current user does not
60
75
  # already have a key on the server.
61
76
  def push!
77
+ passwords_to_try = (Keyman.password_cache ||= [nil]).dup
62
78
  @users.each do |user, objects|
63
79
  begin
64
- Timeout.timeout(10) do |t|
65
- Net::SSH.start(self.host, user) do |ssh|
66
- ssh.exec!("mkdir -p ~/.ssh")
67
- file = authorized_keys(user).gsub("\n", "\\n").gsub("\t", "\\t")
68
- ssh.exec!("echo -e '#{file}' > ~/.ssh/authorized_keys")
80
+ passwords_to_try.each do |password|
81
+ Timeout.timeout(10) do |t|
82
+ Net::SSH.start(self.host, user, :password => password) do |ssh|
83
+ ssh.exec!("mkdir -p ~/.ssh")
84
+ file = authorized_keys(user).gsub("\n", "\\n").gsub("\t", "\\t")
85
+ ssh.exec!("echo -e '#{file}' > ~/.ssh/authorized_keys")
86
+ end
87
+ Keyman.password_cache << password if password
69
88
  end
70
89
  end
71
90
  puts "\e[32mPushed authorized_keys to #{user}@#{self.host}\e[0m"
91
+ rescue Net::SSH::AuthenticationFailed
92
+ passwords_to_try.shift
93
+ if passwords_to_try.empty?
94
+ puts "\e[35mAuthorization failed on to #{user}@#{self.host}.\e[0m"
95
+ password = HighLine.ask("Enter password: ") { |q| q.echo = "*" }
96
+ if password.length > 0
97
+ passwords_to_try << password
98
+ retry
99
+ else
100
+ puts "\e[37mSkipping #{user}@#{self.host}\e[0m"
101
+ end
102
+ else
103
+ retry
104
+ end
72
105
  rescue Timeout::Error
73
106
  puts "\e[31mTimed out while uploading authorized_keys to #{user}@#{self.host}\e[0m"
74
- rescue
75
- puts "\e[31mFailed to upload authorized_keys to #{user}@#{self.host}\e[0m"
107
+ rescue => e
108
+ puts "\e[31mFailed to upload authorized_keys to #{user}@#{self.host} (#{e.class})\e[0m"
109
+ puts e.message
76
110
  end
77
111
  end
78
112
  end
@@ -0,0 +1,29 @@
1
+ module Keyman
2
+ class ServerGroup
3
+
4
+ attr_accessor :servers, :users, :name
5
+
6
+ def initialize
7
+ @servers = []
8
+ @users = {}
9
+ end
10
+
11
+ def self.add(name, &block)
12
+ s = self.new
13
+ s.name = name
14
+ s.instance_eval(&block)
15
+ Keyman.server_groups << s
16
+ s
17
+ end
18
+
19
+ def server(host, location = nil)
20
+ @servers << Server.add_by_name(host, :users => @users, :group => self)
21
+ end
22
+
23
+ def user(name, *access_objects)
24
+ @users[name] = access_objects
25
+ @servers.each { |s| s.user(name, *access_objects) }
26
+ end
27
+
28
+ end
29
+ end
@@ -9,6 +9,10 @@ module Keyman
9
9
  if existing
10
10
  existing
11
11
  else
12
+ if existing = Keyman.user_or_group_for(name)
13
+ raise Error, "#{existing.class.to_s.split('::').last} already exists for '#{name}' - cannot define user with this name."
14
+ end
15
+
12
16
  u = self.new
13
17
  u.name = name.to_sym
14
18
  u.key = key
@@ -0,0 +1,22 @@
1
+ # Example Keyman server configuration file
2
+ # This file should contain all your server & server group definitions which should
3
+ # be used within this manifest.
4
+ #
5
+ # You may also create other files with the extension .km within your manifest directory.
6
+ # These must only contain server/server group definitions.
7
+ #
8
+ # server_group :app_servers do
9
+ # server 'app01.myapplication.com'
10
+ # server 'app02.myapplication.com'
11
+ # server 'app03.myapplication.com'
12
+ #
13
+ # user 'root', :admins
14
+ # user 'app', :admins, :staff
15
+ # end
16
+ #
17
+ # server do
18
+ # host 'db-a.myapplication.com'
19
+ # location :london
20
+ # user 'root', :admins
21
+ # user 'db', :admins, :database_admins
22
+ # end
@@ -0,0 +1,8 @@
1
+ # Example Keyman user/group configuration file
2
+ # This file should contain all your user & group definitions which should
3
+ # be used within this manifest.
4
+ #
5
+ # group :admins do
6
+ # user :adam, 'ssh-rsa AAAAA=='
7
+ # user :dave, 'ssh-rsa BBBBB=='
8
+ # end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keyman
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-29 00:00:00.000000000 Z
12
+ date: 2013-02-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: net-ssh
@@ -27,6 +27,22 @@ dependencies:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
29
  version: 2.6.3
30
+ - !ruby/object:Gem::Dependency
31
+ name: highline
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
30
46
  description: A simple library for managing distributed SSH keys
31
47
  email: adam@atechmedia.com
32
48
  executables:
@@ -39,11 +55,14 @@ files:
39
55
  - Gemfile.lock
40
56
  - keyman.gemspec
41
57
  - lib/keyman/group.rb
42
- - lib/keyman/keyfile.rb
58
+ - lib/keyman/manifest.rb
43
59
  - lib/keyman/server.rb
60
+ - lib/keyman/server_group.rb
44
61
  - lib/keyman/user.rb
45
62
  - lib/keyman.rb
46
63
  - README.md
64
+ - templates/servers.km
65
+ - templates/users.km
47
66
  homepage: http://atechmedia.com
48
67
  licenses: []
49
68
  post_install_message:
@@ -1,19 +0,0 @@
1
- module Keyman
2
- class Keyfile
3
-
4
- class << self
5
- def group(name, &users_block)
6
- Group.add(name, &users_block)
7
- end
8
-
9
- def server(&block)
10
- Server.add(&block)
11
- end
12
-
13
- def user(username, key)
14
- User.add(username, key)
15
- end
16
- end
17
-
18
- end
19
- end