foreman_docker 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +84 -7
- data/app/assets/javascripts/foreman_docker/image_step.js +56 -0
- data/app/assets/stylesheets/foreman_docker/autocomplete.css.scss +3 -0
- data/app/controllers/containers/steps_controller.rb +11 -4
- data/app/controllers/containers_controller.rb +59 -2
- data/app/helpers/containers_helper.rb +6 -6
- data/app/models/container.rb +9 -13
- data/app/models/docker_image.rb +2 -0
- data/app/models/docker_tag.rb +1 -0
- data/app/models/foreman_docker/docker.rb +31 -12
- data/app/views/containers/_list.html.erb +3 -1
- data/app/views/containers/show.html.erb +42 -0
- data/app/views/containers/steps/configuration.html.erb +8 -4
- data/app/views/containers/steps/image.html.erb +30 -18
- data/config/routes.rb +5 -0
- data/db/migrate/20141028164206_change_memory_in_container.rb +9 -0
- data/db/migrate/20141028164633_change_cpuset_in_container.rb +9 -0
- data/lib/foreman_docker/engine.rb +11 -4
- data/lib/foreman_docker/version.rb +1 -1
- data/test/functionals/container_controller_test.rb +13 -0
- data/test/functionals/containers_steps_controller_test.rb +24 -10
- data/test/units/container_test.rb +11 -0
- data/test/{models → units}/docker_image_test.rb +11 -0
- data/test/units/docker_tag_test.rb +35 -0
- metadata +20 -44
- data/test/models/container_test.rb +0 -36
- data/test/models/docker_tag_test.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e7371179124d8fa414db7e60fbcab48110a4372
|
4
|
+
data.tar.gz: 339e3c491804ae9432363136be744a48346af11c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 08356581341a4c80c452ae75dc851ef81c9ec51c61df9106c44d99d8f157ceda5bdf1f0d325e6182983f8591d602f1998618f0058704a3791105465eb0a4d044
|
7
|
+
data.tar.gz: b0c79637f96a572eadf4d82c2a322669d8152690e4a956be0696bd06ee32b82ab93d87dca0f5891799dc1cf58671f5043283a12d33d98a7d2ffe16a204e7ce00
|
data/README.md
CHANGED
@@ -1,6 +1,39 @@
|
|
1
1
|
# Foreman Docker Plugin
|
2
2
|
|
3
|
-
|
3
|
+
```foreman_docker``` enables provisioning and managing of [Docker](http://docker.com) containers and images in [Foreman](http://github.com/theforeman/foreman), all of that under the GPL v3+ license.
|
4
|
+
|
5
|
+
* Website: [TheForeman.org](http://theforeman.org)
|
6
|
+
* ServerFault tag: [Foreman](http://serverfault.com/questions/tagged/foreman)
|
7
|
+
* Issues: [foreman_docker Redmine](http://projects.theforeman.org/projects/docker/issues)
|
8
|
+
* Wiki: [Foreman wiki](http://projects.theforeman.org/projects/foreman/wiki/About)
|
9
|
+
* Community and support: #theforeman for general support, #theforeman-dev for development chat in [Freenode](irc.freenode.net)
|
10
|
+
* Mailing lists:
|
11
|
+
* [foreman-users](https://groups.google.com/forum/?fromgroups#!forum/foreman-users)
|
12
|
+
* [foreman-dev](https://groups.google.com/forum/?fromgroups#!forum/foreman-dev)
|
13
|
+
|
14
|
+
## Features
|
15
|
+
|
16
|
+
* Special view with logs and processes of Foreman managed containers
|
17
|
+
![](http://i.imgur.com/D21bdgj.png)
|
18
|
+
![](http://i.imgur.com/XnrPTZC.png)
|
19
|
+
* Wizard for container creation and cgroups configuration
|
20
|
+
![Select a docker image](http://i.imgur.com/IoMuNnr.png)
|
21
|
+
![Cgroups configuration](http://i.imgur.com/74d99Tf.png)
|
22
|
+
* Commit and upload containers: creates an image with the status of your current container
|
23
|
+
![Commit and upload to the docker hub](http://i.imgur.com/coF5Y0L.png)
|
24
|
+
* Container listing and basic CRUD operations
|
25
|
+
![](http://i.imgur.com/DPcaHkZ.png)
|
26
|
+
|
27
|
+
### Planned
|
28
|
+
* [Kubernetes](https://github.com/GoogleCloudPlatform/kubernetes/) integration
|
29
|
+
* Events stream ([#8037](http://projects.theforeman.org/issues/8037))
|
30
|
+
* Tight integration between Docker hosts [Atomic](http://www.projectatomic.io/) and [CoreOS](http://coreos.com/) and containers ([#7653](http://projects.theforeman.org/issues/7653), [#7652](http://projects.theforeman.org/issues/7652))
|
31
|
+
* Quickstart images - pre-supplied images and configuration ([#7869](http://projects.theforeman.org/issues/7869))
|
32
|
+
* Environment variables support ([#8226](http://projects.theforeman.org/issues/8226))
|
33
|
+
* Support to expose ports during creation or at runtime ([#7864](http://projects.theforeman.org/issues/7864))
|
34
|
+
* Links to other containers ([#7866](http://projects.theforeman.org/issues/7866))
|
35
|
+
* API ([#7874](http://projects.theforeman.org/issues/7874))
|
36
|
+
* [Hammer CLI](http://github.com/theforeman/hammer-cli-foreman) support ([#8227](http://projects.theforeman.org/issues/8227))
|
4
37
|
|
5
38
|
## Installation
|
6
39
|
|
@@ -8,19 +41,62 @@ Please see the Foreman manual for appropriate instructions:
|
|
8
41
|
|
9
42
|
* [Foreman: How to Install a Plugin](http://theforeman.org/manuals/latest/index.html#6.1InstallaPlugin)
|
10
43
|
|
11
|
-
|
44
|
+
### Red Hat, CentOS, Fedora, Scientific Linux (rpm)
|
12
45
|
|
13
|
-
|
46
|
+
Set up the repo as explained in the link above, then run
|
47
|
+
|
48
|
+
# yum install ruby193-rubygem-foreman_docker
|
49
|
+
|
50
|
+
### Debian, Ubuntu (deb)
|
51
|
+
|
52
|
+
Set up the repo as explained in the link above, then run
|
53
|
+
|
54
|
+
# apt-get install ruby-foreman-docker
|
55
|
+
|
56
|
+
### Bundle (gem)
|
57
|
+
|
58
|
+
Add the following to bundler.d/Gemfile.local.rb in your Foreman installation directory (/usr/share/foreman by default)
|
59
|
+
|
60
|
+
$ gem 'foreman_docker'
|
61
|
+
|
62
|
+
Then run `bundle install` and `foreman-rake db:migrate` from the same directory
|
63
|
+
|
64
|
+
--------------
|
65
|
+
|
66
|
+
To verify that the installation was successful, go to Foreman, top bar **Administer > About** and check 'foreman_docker' shows up in the **System Status** menu under the Plugins tab. You should also see a **'Containers'** button show up in the top bar, similar to this
|
67
|
+
|
68
|
+
![](http://i.imgur.com/Ug14Ktl.png)
|
69
|
+
|
70
|
+
## Configuration
|
71
|
+
|
72
|
+
Go to **Infrastructure > Compute Resources** and click on "New Compute Resource".
|
73
|
+
|
74
|
+
Choose the **Docker provider**, and fill in all the fields. User name, password, and email are used so that Docker clients such as Foreman can make the host download images from the Docker hub. Your password will be encrypted in the database.
|
75
|
+
|
76
|
+
That's it. You're now ready to create and manage containers in your new Docker compute resource.
|
14
77
|
|
15
78
|
## Compatibility
|
16
79
|
|
17
|
-
| Foreman
|
80
|
+
| Foreman | Plugin |
|
18
81
|
| ---------------:| --------------:|
|
19
|
-
| >=
|
82
|
+
| >= 1.5 | 0.0.1 - 0.0.3 |
|
83
|
+
| >= 1.6 | 0.1.0 - 0.2.0 |
|
20
84
|
|
21
|
-
##
|
85
|
+
## Known bugs
|
86
|
+
* Unsaved new containers leave a dangling container object in the database
|
87
|
+
* Power operations redirect to compute resource container view even for managed container
|
22
88
|
|
23
|
-
|
89
|
+
## How to contribute?
|
90
|
+
|
91
|
+
Generally, follow the [Foreman guidelines](http://theforeman.org/contribute.html). For code-related contributions, fork this project and send a pull request with all changes. Some things to keep in mind:
|
92
|
+
* [Follow the rules](http://theforeman.org/contribute.html#SubmitPatches) about commit message style and create a Redmine issue. Doing this right will help reviewers to get your contribution merged faster.
|
93
|
+
* [Rubocop](https://github.com/bbatsov/rubocop) will analyze your code, you can run it locally with `rake rubocop`.
|
94
|
+
* All of our pull requests run the full test suite in our [Jenkins CI system](http://ci.theforeman.org/). Please include tests in your pull requests for any additions or changes in functionality
|
95
|
+
|
96
|
+
|
97
|
+
### Testing
|
98
|
+
|
99
|
+
Run `rake test:docker` from your Foreman directory to run the test suite.
|
24
100
|
|
25
101
|
## Latest code
|
26
102
|
|
@@ -44,3 +120,4 @@ GNU General Public License for more details.
|
|
44
120
|
|
45
121
|
You should have received a copy of the GNU General Public License
|
46
122
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
123
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
$(document).ready(function() {
|
2
|
+
var tag = $('#tag');
|
3
|
+
tag.autocomplete({
|
4
|
+
source: [],
|
5
|
+
delay: 500,
|
6
|
+
minLength: 0
|
7
|
+
}).focus( function() {
|
8
|
+
$(this).data("uiAutocomplete").search($(this).val());
|
9
|
+
});
|
10
|
+
|
11
|
+
var target = $('#search');
|
12
|
+
autoCompleteImage(target);
|
13
|
+
target.autocomplete({
|
14
|
+
source: function( request, response ) { autoCompleteImage(target); },
|
15
|
+
delay: 500,
|
16
|
+
minLength: 1
|
17
|
+
});
|
18
|
+
});
|
19
|
+
|
20
|
+
function autoCompleteImage(item) {
|
21
|
+
$.ajax({
|
22
|
+
type:'get',
|
23
|
+
url: $(item).attr('data-url'),
|
24
|
+
data:'search=' + item.val(),
|
25
|
+
success:function (result) {
|
26
|
+
if(result == 'true'){
|
27
|
+
$('#search-addon').attr('title', 'Image found in the compute resource');
|
28
|
+
$('#search-addon').removeClass('glyphicon-remove');
|
29
|
+
$('#search-addon').css('color', 'lightgreen');
|
30
|
+
$('#search-addon').addClass('glyphicon-ok');
|
31
|
+
setAutocompleteTags();
|
32
|
+
} else {
|
33
|
+
$('#search-addon').attr('title', 'Image NOT found in the compute resource');
|
34
|
+
$('#search-addon').removeClass('glyphicon-ok');
|
35
|
+
$('#search-addon').css('color', 'red');
|
36
|
+
$('#search-addon').addClass('glyphicon-remove');
|
37
|
+
$('#tag').autocomplete('option', 'source', []);
|
38
|
+
}
|
39
|
+
}
|
40
|
+
});
|
41
|
+
}
|
42
|
+
|
43
|
+
function setAutocompleteTags() {
|
44
|
+
var tag = $('#tag');
|
45
|
+
tag.addClass('tags-autocomplete-loading');
|
46
|
+
tag.val('');
|
47
|
+
var source = [];
|
48
|
+
$.getJSON( tag.data("url"), { search: $('#search').val() },
|
49
|
+
function(data) {
|
50
|
+
tag.removeClass('tags-autocomplete-loading');
|
51
|
+
$.each( data, function(index, value) {
|
52
|
+
source.push({label: value.label, value: value.value});
|
53
|
+
});
|
54
|
+
});
|
55
|
+
tag.autocomplete('option', 'source', source);
|
56
|
+
}
|
@@ -21,13 +21,20 @@ module Containers
|
|
21
21
|
when :preliminary
|
22
22
|
@container.update_attribute(:compute_resource_id, params[:container][:compute_resource_id])
|
23
23
|
when :image
|
24
|
-
@container.
|
25
|
-
|
24
|
+
@container.update_attributes!(
|
25
|
+
:image => (image = DockerImage.find_or_create_by_image_id!(params[:image])),
|
26
|
+
:tag => DockerTag.find_or_create_by_tag_and_docker_image_id!(params[:container][:tag],
|
27
|
+
image.id))
|
26
28
|
when :configuration
|
27
29
|
@container.update_attributes(params[:container])
|
28
30
|
when :environment
|
29
31
|
@container.update_attributes(params[:container])
|
30
|
-
|
32
|
+
if (response = start_container)
|
33
|
+
@container.uuid = response.id
|
34
|
+
else
|
35
|
+
process_error(:object => @container.compute_resource, :render => 'environment')
|
36
|
+
return
|
37
|
+
end
|
31
38
|
end
|
32
39
|
render_wizard @container
|
33
40
|
end
|
@@ -49,7 +56,7 @@ module Containers
|
|
49
56
|
end
|
50
57
|
|
51
58
|
def start_container
|
52
|
-
@container.compute_resource.
|
59
|
+
@container.compute_resource.create_container(@container.parametrize)
|
53
60
|
end
|
54
61
|
end
|
55
62
|
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
class ContainersController < ::ApplicationController
|
2
|
-
before_filter :
|
2
|
+
before_filter :find_container, :only => [:show,
|
3
|
+
:auto_complete_image,
|
4
|
+
:auto_complete_image_tags,
|
5
|
+
:commit]
|
3
6
|
|
4
7
|
def index
|
5
8
|
@container_resources = allowed_resources.select { |cr| cr.provider == 'Docker' }
|
@@ -20,7 +23,8 @@ class ContainersController < ::ApplicationController
|
|
20
23
|
def destroy
|
21
24
|
if resource_deletion
|
22
25
|
process_success(:success_redirect => containers_path,
|
23
|
-
:success_msg => _("Container
|
26
|
+
:success_msg => (_("Container %s is being deleted.") %
|
27
|
+
@deleted_identifier))
|
24
28
|
else
|
25
29
|
process_error(:redirect => containers_path)
|
26
30
|
end
|
@@ -31,8 +35,51 @@ class ContainersController < ::ApplicationController
|
|
31
35
|
def show
|
32
36
|
end
|
33
37
|
|
38
|
+
def auto_complete_image
|
39
|
+
if @container.compute_resource.exist?(params[:search])
|
40
|
+
render :text => 'true'
|
41
|
+
else
|
42
|
+
render :text => 'false'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def auto_complete_image_tags
|
47
|
+
images = @container.compute_resource.all_images(params[:search]).map do |image|
|
48
|
+
image.info['RepoTags'].map do |image_tag|
|
49
|
+
_, tag = image_tag.split(':')
|
50
|
+
{ :label => CGI.escapeHTML(tag), :value => CGI.escapeHTML(tag) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
render :json => images.flatten
|
54
|
+
end
|
55
|
+
|
56
|
+
def commit
|
57
|
+
Docker::Container.get(@container.uuid).commit(:author => params[:commit][:author],
|
58
|
+
:repo => params[:commit][:repo],
|
59
|
+
:tag => params[:commit][:tag],
|
60
|
+
:comment => params[:commit][:comment])
|
61
|
+
|
62
|
+
process_success :success_redirect => :back,
|
63
|
+
:success_msg => _("%{container} commit was successful") %
|
64
|
+
{ :container => @container }
|
65
|
+
rescue => e
|
66
|
+
process_error :redirect => :back, :error_msg => _("Failed to commit %{container}: %{e}") %
|
67
|
+
{ :container => @container, :e => e }
|
68
|
+
end
|
69
|
+
|
34
70
|
private
|
35
71
|
|
72
|
+
def action_permission
|
73
|
+
case params[:action]
|
74
|
+
when 'auto_complete_image', 'auto_complete_image_tags'
|
75
|
+
:view
|
76
|
+
when 'commit'
|
77
|
+
:commit
|
78
|
+
else
|
79
|
+
super
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
36
83
|
def resource_deletion
|
37
84
|
# Unmanaged container - only present in Compute Resource
|
38
85
|
if params[:compute_resource_id].present?
|
@@ -59,4 +106,14 @@ class ContainersController < ::ApplicationController
|
|
59
106
|
def allowed_resources
|
60
107
|
ComputeResource.authorized(:view_compute_resources)
|
61
108
|
end
|
109
|
+
|
110
|
+
# To be replaced by find_resource after 1.6 support is deprecated
|
111
|
+
def find_container
|
112
|
+
if params[:id].blank?
|
113
|
+
not_found
|
114
|
+
return
|
115
|
+
end
|
116
|
+
@container = Container.authorized("#{action_permission}_#{controller_name}".to_sym)
|
117
|
+
.find(params[:id])
|
118
|
+
end
|
62
119
|
end
|
@@ -33,12 +33,7 @@ module ContainersHelper
|
|
33
33
|
@compute_resource = container.compute_resource
|
34
34
|
title_actions(
|
35
35
|
button_group(
|
36
|
-
|
37
|
-
.merge(:auth_object => container,
|
38
|
-
:permission => 'commit_containers'),
|
39
|
-
:title => _('Saves differences between image' \
|
40
|
-
'and current state of container' \
|
41
|
-
'as a new image'))
|
36
|
+
link_to(_('Commit'), '#commit-modal', :'data-toggle' => 'modal')
|
42
37
|
),
|
43
38
|
button_group(vm_power_action(container.in_fog)),
|
44
39
|
button_group(
|
@@ -51,4 +46,9 @@ module ContainersHelper
|
|
51
46
|
)
|
52
47
|
)
|
53
48
|
end
|
49
|
+
|
50
|
+
def auto_complete_search(name, val, options = {})
|
51
|
+
addClass options, 'form-control'
|
52
|
+
text_field_tag(name, val, options)
|
53
|
+
end
|
54
54
|
end
|
data/app/models/container.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
class Container < ActiveRecord::Base
|
2
|
+
include Authorizable
|
3
|
+
|
2
4
|
belongs_to :compute_resource
|
3
5
|
belongs_to :image, :class_name => 'DockerImage', :foreign_key => 'docker_image_id'
|
4
6
|
belongs_to :tag, :class_name => 'DockerTag', :foreign_key => 'docker_tag_id'
|
@@ -8,19 +10,13 @@ class Container < ActiveRecord::Base
|
|
8
10
|
:attach_stdout, :attach_stderr, :tag, :uuid
|
9
11
|
|
10
12
|
def parametrize
|
11
|
-
{
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
self[:docker_image_id] = DockerImage.find_or_create_by_image_id!(image_id).id
|
19
|
-
end
|
20
|
-
|
21
|
-
def tag=(tag_name)
|
22
|
-
self[:docker_tag_id] = DockerTag
|
23
|
-
.find_or_create_by_tag_and_docker_image_id!(tag_name, image.id).id
|
13
|
+
{ 'name' => name, # key has to be lower case to be picked up by the Docker API
|
14
|
+
'Image' => tag.tag.blank? ? image.image_id : "#{image.image_id}:#{tag.tag}",
|
15
|
+
'Tty' => tty, 'Memory' => memory,
|
16
|
+
'Entrypoint' => entrypoint.try(:split), 'Cmd' => command.try(:split),
|
17
|
+
'AttachStdout' => attach_stdout, 'AttachStdin' => attach_stdin,
|
18
|
+
'AttachStderr' => attach_stderr, 'CpuShares' => cpu_shares,
|
19
|
+
'Cpuset' => cpu_set }
|
24
20
|
end
|
25
21
|
|
26
22
|
def in_fog
|
data/app/models/docker_image.rb
CHANGED
data/app/models/docker_tag.rb
CHANGED
@@ -20,34 +20,43 @@ module ForemanDocker
|
|
20
20
|
super.merge(:mac => :mac)
|
21
21
|
end
|
22
22
|
|
23
|
-
# FIXME
|
24
|
-
def max_cpu_count
|
25
|
-
8
|
26
|
-
end
|
27
|
-
|
28
23
|
def max_memory
|
29
24
|
16 * 1024 * 1024 * 1024
|
30
25
|
end
|
31
26
|
|
32
27
|
def available_images
|
33
|
-
client.images
|
28
|
+
client.images.all
|
29
|
+
end
|
30
|
+
|
31
|
+
def all_images(filter = '')
|
32
|
+
client # initialize Docker-Api
|
33
|
+
# we are using an older version of docker-api, which differs from the current
|
34
|
+
::Docker::Image.all('filter' => filter)
|
35
|
+
end
|
36
|
+
|
37
|
+
def exist?(name)
|
38
|
+
::Docker::Image.exist?(name)
|
34
39
|
end
|
35
40
|
|
36
41
|
def image(id)
|
37
|
-
client.
|
42
|
+
client.image_get(id)
|
43
|
+
end
|
44
|
+
|
45
|
+
def search(term = '')
|
46
|
+
client.images.image_search(:term => term)
|
38
47
|
end
|
39
48
|
|
40
49
|
def provider_friendly_name
|
41
50
|
'Docker'
|
42
51
|
end
|
43
52
|
|
44
|
-
def
|
53
|
+
def create_container(args = {})
|
45
54
|
options = vm_instance_defaults.merge(args)
|
46
|
-
logger.debug("
|
47
|
-
|
48
|
-
rescue Excon::Errors::
|
55
|
+
logger.debug("Creating container with the following options: #{options.inspect}")
|
56
|
+
::Docker::Container.create(options)
|
57
|
+
rescue Excon::Errors::Error, ::Docker::Error::DockerError => e
|
49
58
|
logger.debug "Fog error: #{e.message}\n " + e.backtrace.join("\n ")
|
50
|
-
errors.add(:base, e.message.to_s)
|
59
|
+
errors.add(:base, _("Error creating container. Check the Foreman logs: %s") % e.message.to_s)
|
51
60
|
false
|
52
61
|
end
|
53
62
|
|
@@ -56,6 +65,16 @@ module ForemanDocker
|
|
56
65
|
'cmd' => ['/bin/bash'])
|
57
66
|
end
|
58
67
|
|
68
|
+
def console(uuid)
|
69
|
+
test_connection
|
70
|
+
container = ::Docker::Container.get(uuid)
|
71
|
+
{
|
72
|
+
:name => container.info['Name'],
|
73
|
+
'timestamp' => Time.now.utc,
|
74
|
+
'output' => container.logs(:stdout => true, :tail => 100)
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
59
78
|
def test_connection(options = {})
|
60
79
|
super
|
61
80
|
client
|
@@ -102,3 +102,45 @@
|
|
102
102
|
<% end %>
|
103
103
|
</div>
|
104
104
|
</div>
|
105
|
+
|
106
|
+
<div id="commit-modal" class="modal fade">
|
107
|
+
<div class="modal-dialog">
|
108
|
+
<div class="modal-content">
|
109
|
+
<div class="modal-header">
|
110
|
+
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
111
|
+
<h4 class="modal-title"><%= _('Commit this container state') %></h4>
|
112
|
+
</div>
|
113
|
+
<div class="modal-body">
|
114
|
+
This will save your current container state to an image.
|
115
|
+
<hr/>
|
116
|
+
<%= form_tag commit_container_path(:id => @container.id), :id => 'commit-form', :class => 'form-horizontal' do %>
|
117
|
+
<div class="form-group">
|
118
|
+
<%= label_tag "commit[repo]", _("Repo"), :class=>"col-sm-2 control-label" %>
|
119
|
+
<%= text_field :commit, :repo, { :class => "col-sm-8", :focus_on_load => true,
|
120
|
+
:placeholder => _('docker/my-committed-image') } %>
|
121
|
+
</div>
|
122
|
+
<div class="form-group">
|
123
|
+
<%= label_tag "commit[tag]", _("Tag"), :class=>"col-sm-2 control-label" %>
|
124
|
+
<%= text_field :commit, :tag, { :class => "col-sm-8", :focus_on_load => true,
|
125
|
+
:placeholder => _('latest') } %>
|
126
|
+
</div>
|
127
|
+
<div class="form-group">
|
128
|
+
<%= label_tag "commit[author]", _("Author"), :class=>"col-sm-2 control-label" %>
|
129
|
+
<%= text_field :commit, :author, { :class => "col-sm-8",
|
130
|
+
:placeholder => _('Foreman user <foremaner@theforeman.org>') } %>
|
131
|
+
</div>
|
132
|
+
<div class="form-group">
|
133
|
+
<%= label_tag "commit[comment]", _("Comment"), :class=>"col-sm-2 control-label" %>
|
134
|
+
<%= text_field :commit, :comment, { :class => "col-sm-8",
|
135
|
+
:placeholder => _('Description of the commit') } %>
|
136
|
+
</div>
|
137
|
+
<div class="modal-footer">
|
138
|
+
<button type="button" class="btn btn-default" data-dismiss="modal"><%= _('Cancel') %></button>
|
139
|
+
<%= button_tag(:type => 'submit', :class => 'btn btn-primary') do %>
|
140
|
+
<%= _('Submit') %> <span class="glyphicon glyphicon-cloud-upload"></span>
|
141
|
+
<% end %>
|
142
|
+
</div>
|
143
|
+
<% end %>
|
144
|
+
</div>
|
145
|
+
</div>
|
146
|
+
</div>
|
@@ -3,12 +3,16 @@
|
|
3
3
|
<h3><%= _("Basic options") %></h3>
|
4
4
|
<%= text_f f, :name, :size => 'col-md-4' %>
|
5
5
|
<%= text_f f, :command, :size => 'col-md-4' %>
|
6
|
-
<%= text_f f, :entrypoint, :size => 'col-md-4' %>
|
6
|
+
<%= text_f f, :entrypoint, :size => 'col-md-4', :placeholder => '/bin/sh -c by default' %>
|
7
7
|
<hr>
|
8
8
|
<h3><%= _("Compute options") %></h3>
|
9
|
-
<%=
|
10
|
-
|
11
|
-
|
9
|
+
<%= text_f f, :cpu_set, :class => "col-md-2", :label => _('CPU sets'),
|
10
|
+
:placeholder => '0-2,16 represents CPUs 0, 1, 2, and 16.',
|
11
|
+
:help_block => link_to(_("learn more about CPU sets"), 'http://docs.fedoraproject.org/en-US/Fedora/17/html/Resource_Management_Guide/sec-cpuset.html') %>
|
12
|
+
<%= text_f f, :cpu_shares, :class => "col-md-2", :label => _('CPU shares'),
|
13
|
+
:placeholder => 'relative share of CPU time in cgroup',
|
14
|
+
:help_block => link_to(_("learn more about CPU shares"), 'http://docs.fedoraproject.org/en-US/Fedora/17/html/Resource_Management_Guide/sec-cpuset.html') %>
|
15
|
+
<%= text_f f, :memory, :class => "col-md-2", :label => _('Memory'), :placeholder => '512m limits container memory usage to 512 MB' %>
|
12
16
|
<%= render :partial => 'form_buttons' %>
|
13
17
|
<% end %>
|
14
18
|
<% end %>
|
@@ -1,33 +1,45 @@
|
|
1
|
+
<%= javascript 'foreman_docker/image_step' %>
|
2
|
+
<%= stylesheet 'foreman_docker/autocomplete' %>
|
1
3
|
<%= render :layout => 'title', :locals => { :step => 2 } do %>
|
4
|
+
<%= form_for @container, :url => wizard_path, :method => :put do |f| %>
|
5
|
+
|
2
6
|
<ul class="nav nav-tabs" data-tabs="tabs">
|
3
7
|
<li class="active"><a href="#primary" data-toggle="tab">
|
4
8
|
<span class="glyphicon glyphicon-cloud-download"></span>
|
5
9
|
<%= _("Docker hub") %>
|
6
10
|
</a></li>
|
7
|
-
<li><a href="#others" data-toggle="tab">
|
8
|
-
<span class="glyphicon glyphicon-globe"></span>
|
9
|
-
<%= _("Others") %>
|
10
|
-
</a></li>
|
11
11
|
</ul>
|
12
|
+
|
12
13
|
<div class="tab-content">
|
13
|
-
|
14
|
-
<div class="
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
14
|
+
<div class="tab-pane active" id="hub">
|
15
|
+
<div class="input-group col-md-6">
|
16
|
+
<%= auto_complete_search(:image, '',
|
17
|
+
:'data-url' => auto_complete_image_container_path(@container),
|
18
|
+
:value => f.object.image.present? ? f.object.image.image_id : '',
|
19
|
+
:id => :search,
|
20
|
+
:placeholder => _('Find your favorite container, e.g: centos:latest')) %>
|
21
|
+
<span class="input-group-addon glyphicon" id="search-addon"></span>
|
22
|
+
<span class="input-group-btn">
|
23
|
+
<%= button_tag(:class => 'btn btn-default',
|
24
|
+
:type => 'button',
|
25
|
+
:onclick => "$('#search').trigger('focus')") do %>
|
22
26
|
<span class="glyphicon glyphicon-search"></span>
|
23
27
|
<%= _("Search") %>
|
24
28
|
<% end %>
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
+
</span>
|
30
|
+
</div>
|
31
|
+
<br/>
|
32
|
+
<div class="input-group col-md-6">
|
33
|
+
<%= text_f f, :tag,
|
34
|
+
:size => 'col-md-6',
|
35
|
+
:value => f.object.tag.present? ? f.object.tag.tag : '',
|
36
|
+
:id => 'tag',
|
37
|
+
:'data-url' => auto_complete_image_tags_container_path(@container) %>
|
29
38
|
</div>
|
39
|
+
<hr/>
|
30
40
|
<%= render :partial => 'form_buttons' %>
|
31
|
-
|
41
|
+
</div>
|
32
42
|
</div>
|
43
|
+
|
44
|
+
<% end %>
|
33
45
|
<% end %>
|
data/config/routes.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
Rails.application.routes.draw do
|
2
2
|
resources :containers, :only => [:index, :new, :show, :destroy] do
|
3
|
+
member do
|
4
|
+
post :commit
|
5
|
+
end
|
3
6
|
resources :steps, :controller => 'containers/steps', :only => [:show, :update]
|
7
|
+
get :auto_complete_image, :on => :member
|
8
|
+
get :auto_complete_image_tags, :on => :member
|
4
9
|
end
|
5
10
|
end
|
@@ -3,22 +3,26 @@ require 'gettext_i18n_rails'
|
|
3
3
|
require 'fog'
|
4
4
|
require 'fog/fogdocker'
|
5
5
|
require 'wicked'
|
6
|
+
require 'docker'
|
6
7
|
|
7
8
|
module ForemanDocker
|
8
9
|
# Inherit from the Rails module of the parent app (Foreman), not the plugin.
|
9
10
|
# Thus, inherits from ::Rails::Engine and not from Rails::Engine
|
10
11
|
class Engine < ::Rails::Engine
|
12
|
+
engine_name 'foreman_docker'
|
13
|
+
|
11
14
|
initializer 'foreman_docker.load_app_instance_data' do |app|
|
12
15
|
app.config.paths['db/migrate'] += ForemanDocker::Engine.paths['db/migrate'].existent
|
13
16
|
end
|
14
17
|
|
15
18
|
initializer "foreman_docker.assets.precompile" do |app|
|
16
|
-
app.config.assets.precompile +=
|
19
|
+
app.config.assets.precompile += %w(foreman_docker/terminal.css foreman_docker/image_step.js)
|
17
20
|
end
|
18
21
|
|
19
22
|
initializer 'foreman_docker.configure_assets', :group => :assets do
|
20
|
-
SETTINGS[:
|
21
|
-
{ :assets => { :precompile => ['foreman_docker/terminal.css'
|
23
|
+
SETTINGS[:foreman_docker] =
|
24
|
+
{ :assets => { :precompile => ['foreman_docker/terminal.css',
|
25
|
+
'foreman_docker/image_step.js'] } }
|
22
26
|
end
|
23
27
|
|
24
28
|
initializer 'foreman_docker.register_gettext', :after => :load_config_initializers do
|
@@ -44,7 +48,10 @@ module ForemanDocker
|
|
44
48
|
end
|
45
49
|
|
46
50
|
security_block :containers do
|
47
|
-
permission :view_containers, :containers => [:index, :show
|
51
|
+
permission :view_containers, :containers => [:index, :show,
|
52
|
+
:auto_complete_image,
|
53
|
+
:auto_complete_image_tags]
|
54
|
+
permission :commit_containers, :containers => [:commit]
|
48
55
|
permission :create_containers, :'containers/steps' => [:show, :update],
|
49
56
|
:containers => [:new]
|
50
57
|
permission :destroy_containers, :containers => [:destroy]
|
@@ -24,4 +24,17 @@ class ContainersControllerTest < ActionController::TestCase
|
|
24
24
|
:id => container.id }, set_session_user
|
25
25
|
assert_redirected_to containers_path
|
26
26
|
end
|
27
|
+
|
28
|
+
test 'committing a managed container' do
|
29
|
+
container = FactoryGirl.create(:container)
|
30
|
+
request.env['HTTP_REFERER'] = container_path(:id => container.id)
|
31
|
+
commit_hash = { :author => 'a', :repo => 'b', :tag => 'c', :comment => 'd' }
|
32
|
+
|
33
|
+
mock_container = mock
|
34
|
+
::Docker::Container.expects(:get).with(container.uuid).returns(mock_container)
|
35
|
+
mock_container.expects(:commit).with(commit_hash)
|
36
|
+
|
37
|
+
post :commit, { :commit => commit_hash,
|
38
|
+
:id => container.id }, set_session_user
|
39
|
+
end
|
27
40
|
end
|
@@ -18,16 +18,30 @@ module Containers
|
|
18
18
|
assert_equal DockerTag.find_by_tag('latest'), @container.tag
|
19
19
|
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
21
|
+
context 'container creation' do
|
22
|
+
setup do
|
23
|
+
@container.update_attribute(:image, (image = FactoryGirl.create(:docker_image,
|
24
|
+
:image_id => 'centos')))
|
25
|
+
@container.update_attribute(:tag, FactoryGirl.create(:docker_tag, :image => image,
|
26
|
+
:tag => 'latest'))
|
27
|
+
end
|
28
|
+
|
29
|
+
test 'uuid of the created container is saved at the end of the wizard' do
|
30
|
+
Fog.mock!
|
31
|
+
fake_container = @container.compute_resource.send(:client).servers.first
|
32
|
+
ForemanDocker::Docker.any_instance.expects(:create_container).returns(fake_container)
|
33
|
+
put :update, { :id => :environment,
|
34
|
+
:container_id => @container.id }, set_session_user
|
35
|
+
assert_equal fake_container.id, Container.find(@container.id).uuid
|
36
|
+
end
|
37
|
+
|
38
|
+
test 'errors are displayed when container creation fails' do
|
39
|
+
Docker::Container.expects(:create).raises(Docker::Error::DockerError, 'some error')
|
40
|
+
put :update, { :id => :environment,
|
41
|
+
:container_id => @container.id }, set_session_user
|
42
|
+
assert_template 'environment'
|
43
|
+
assert_match(/some error/, flash[:error])
|
44
|
+
end
|
31
45
|
end
|
32
46
|
|
33
47
|
test 'wizard finishes with a redirect to the managed container' do
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'test_plugin_helper'
|
2
|
+
|
3
|
+
class ContainerTest < ActiveSupport::TestCase
|
4
|
+
test 'validations do not happen if inactive' do
|
5
|
+
FactoryGirl.build(:container)
|
6
|
+
end
|
7
|
+
|
8
|
+
test 'attributes are validated when active' do
|
9
|
+
FactoryGirl.build(:container)
|
10
|
+
end
|
11
|
+
end
|
@@ -9,4 +9,15 @@ class DockerImageTest < ActiveSupport::TestCase
|
|
9
9
|
refute DockerImage.exists?(image.id)
|
10
10
|
refute DockerTag.exists?(tag.id)
|
11
11
|
end
|
12
|
+
|
13
|
+
context 'validations' do
|
14
|
+
test 'without image_id is invalid' do
|
15
|
+
refute FactoryGirl.build(:docker_image, :image_id => '').valid?
|
16
|
+
end
|
17
|
+
|
18
|
+
test 'image_id has to be unique' do
|
19
|
+
old_image = FactoryGirl.create(:docker_image)
|
20
|
+
refute FactoryGirl.build(:docker_image, :image_id => old_image.image_id).valid?
|
21
|
+
end
|
22
|
+
end
|
12
23
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'test_plugin_helper'
|
2
|
+
|
3
|
+
class DockerTagTest < ActiveSupport::TestCase
|
4
|
+
test 'creating fails if no image is provided' do
|
5
|
+
tag = FactoryGirl.build(:docker_tag, :image => nil)
|
6
|
+
refute tag.valid?
|
7
|
+
assert tag.errors.size >= 1
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'creating succeeds if an image is provided' do
|
11
|
+
tag = FactoryGirl.build(:docker_tag)
|
12
|
+
tag.image = FactoryGirl.build(:docker_image)
|
13
|
+
|
14
|
+
assert tag.valid?
|
15
|
+
assert tag.save
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'validations' do
|
19
|
+
test 'tag has to be present' do
|
20
|
+
refute FactoryGirl.build(:docker_tag, :tag => '').valid?
|
21
|
+
end
|
22
|
+
|
23
|
+
test 'tag is unique within image scope' do
|
24
|
+
image = FactoryGirl.create(:docker_image)
|
25
|
+
tag = FactoryGirl.create(:docker_tag, :image => image)
|
26
|
+
duplicated_tag = FactoryGirl.build(:docker_tag, :image => image, :tag => tag.tag)
|
27
|
+
refute duplicated_tag.valid?
|
28
|
+
end
|
29
|
+
|
30
|
+
test 'tag is not unique for different images' do
|
31
|
+
tag = FactoryGirl.create(:docker_tag)
|
32
|
+
assert FactoryGirl.build(:docker_tag, :tag => tag.tag).valid?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
metadata
CHANGED
@@ -1,85 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_docker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Lobato, Amos Benari
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-11-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: rake
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - '>='
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - '>='
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: fog
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - '>='
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - '>='
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
13
|
- !ruby/object:Gem::Dependency
|
42
14
|
name: docker-api
|
43
15
|
requirement: !ruby/object:Gem::Requirement
|
44
16
|
requirements:
|
45
17
|
- - ~>
|
46
18
|
- !ruby/object:Gem::Version
|
47
|
-
version: 1.13
|
19
|
+
version: '1.13'
|
48
20
|
type: :runtime
|
49
21
|
prerelease: false
|
50
22
|
version_requirements: !ruby/object:Gem::Requirement
|
51
23
|
requirements:
|
52
24
|
- - ~>
|
53
25
|
- !ruby/object:Gem::Version
|
54
|
-
version: 1.13
|
26
|
+
version: '1.13'
|
55
27
|
- !ruby/object:Gem::Dependency
|
56
28
|
name: wicked
|
57
29
|
requirement: !ruby/object:Gem::Requirement
|
58
30
|
requirements:
|
59
|
-
- -
|
31
|
+
- - ~>
|
60
32
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
33
|
+
version: '1.1'
|
62
34
|
type: :runtime
|
63
35
|
prerelease: false
|
64
36
|
version_requirements: !ruby/object:Gem::Requirement
|
65
37
|
requirements:
|
66
|
-
- -
|
38
|
+
- - ~>
|
67
39
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
40
|
+
version: '1.1'
|
69
41
|
- !ruby/object:Gem::Dependency
|
70
42
|
name: rubocop
|
71
43
|
requirement: !ruby/object:Gem::Requirement
|
72
44
|
requirements:
|
73
45
|
- - ~>
|
74
46
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0.26
|
47
|
+
version: '0.26'
|
76
48
|
type: :development
|
77
49
|
prerelease: false
|
78
50
|
version_requirements: !ruby/object:Gem::Requirement
|
79
51
|
requirements:
|
80
52
|
- - ~>
|
81
53
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0.26
|
54
|
+
version: '0.26'
|
83
55
|
description: Provision and manage Docker containers and images from Foreman.
|
84
56
|
email:
|
85
57
|
- dlobatog@redhat.com, abenari@redhat.com
|
@@ -90,6 +62,8 @@ files:
|
|
90
62
|
- LICENSE
|
91
63
|
- README.md
|
92
64
|
- Rakefile
|
65
|
+
- app/assets/javascripts/foreman_docker/image_step.js
|
66
|
+
- app/assets/stylesheets/foreman_docker/autocomplete.css.scss
|
93
67
|
- app/assets/stylesheets/foreman_docker/terminal.css.scss
|
94
68
|
- app/controllers/containers/steps_controller.rb
|
95
69
|
- app/controllers/containers_controller.rb
|
@@ -126,6 +100,8 @@ files:
|
|
126
100
|
- db/migrate/20141009011026_add_attributes_to_container.rb
|
127
101
|
- db/migrate/20141010173220_create_docker_images.rb
|
128
102
|
- db/migrate/20141018110810_add_uuid_to_containers.rb
|
103
|
+
- db/migrate/20141028164206_change_memory_in_container.rb
|
104
|
+
- db/migrate/20141028164633_change_cpuset_in_container.rb
|
129
105
|
- lib/foreman_docker.rb
|
130
106
|
- lib/foreman_docker/engine.rb
|
131
107
|
- lib/foreman_docker/tasks/test.rake
|
@@ -137,10 +113,10 @@ files:
|
|
137
113
|
- test/factories/docker_tag.rb
|
138
114
|
- test/functionals/container_controller_test.rb
|
139
115
|
- test/functionals/containers_steps_controller_test.rb
|
140
|
-
- test/models/container_test.rb
|
141
|
-
- test/models/docker_image_test.rb
|
142
|
-
- test/models/docker_tag_test.rb
|
143
116
|
- test/test_plugin_helper.rb
|
117
|
+
- test/units/container_test.rb
|
118
|
+
- test/units/docker_image_test.rb
|
119
|
+
- test/units/docker_tag_test.rb
|
144
120
|
homepage: http://github.com/theforeman/foreman-docker
|
145
121
|
licenses:
|
146
122
|
- GPL-3
|
@@ -168,12 +144,12 @@ summary: Provision and manage Docker containers and images from Foreman
|
|
168
144
|
test_files:
|
169
145
|
- test/functionals/containers_steps_controller_test.rb
|
170
146
|
- test/functionals/container_controller_test.rb
|
171
|
-
- test/models/docker_image_test.rb
|
172
|
-
- test/models/container_test.rb
|
173
|
-
- test/models/docker_tag_test.rb
|
174
147
|
- test/factories/docker_image.rb
|
175
148
|
- test/factories/compute_resources.rb
|
176
149
|
- test/factories/docker_tag.rb
|
177
150
|
- test/factories/containers.rb
|
151
|
+
- test/units/docker_image_test.rb
|
152
|
+
- test/units/container_test.rb
|
153
|
+
- test/units/docker_tag_test.rb
|
178
154
|
- test/test_plugin_helper.rb
|
179
155
|
has_rdoc:
|
@@ -1,36 +0,0 @@
|
|
1
|
-
require 'test_plugin_helper'
|
2
|
-
|
3
|
-
class ContainerTest < ActiveSupport::TestCase
|
4
|
-
context 'update attributes' do
|
5
|
-
setup do
|
6
|
-
@container = FactoryGirl.create(:container)
|
7
|
-
end
|
8
|
-
|
9
|
-
test 'update image reuses previously created image' do
|
10
|
-
assert_difference('DockerImage.count', 1) do
|
11
|
-
@container.update_attribute(:image, "centos")
|
12
|
-
end
|
13
|
-
assert_equal "centos", @container.image.image_id
|
14
|
-
refute_nil DockerImage.find_by_image_id("centos")
|
15
|
-
|
16
|
-
assert_difference('DockerImage.count', 1) do
|
17
|
-
@container.update_attribute(:image, "redis")
|
18
|
-
end
|
19
|
-
assert_equal "redis", @container.image.image_id
|
20
|
-
|
21
|
-
assert_difference('DockerImage.count', 0) do
|
22
|
-
@container.update_attribute(:image, "centos")
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
test "update tag uses container's associated image" do
|
27
|
-
@container.update_attribute(:image, 'centos')
|
28
|
-
assert_difference('DockerTag.count', 1) do
|
29
|
-
@container.update_attribute(:tag, 'latest')
|
30
|
-
end
|
31
|
-
|
32
|
-
assert_equal 'latest', @container.tag.tag
|
33
|
-
assert_equal @container.tag.image, @container.image
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
require 'test_plugin_helper'
|
2
|
-
|
3
|
-
class DockerTagTest < ActiveSupport::TestCase
|
4
|
-
test 'creating fails if no image is provided' do
|
5
|
-
tag = DockerTag.new(FactoryGirl.attributes_for(:docker_tag))
|
6
|
-
refute tag.valid?
|
7
|
-
assert tag.errors.size >= 1
|
8
|
-
end
|
9
|
-
|
10
|
-
test 'creating succeeds if an image is provided' do
|
11
|
-
tag = FactoryGirl.build(:docker_tag)
|
12
|
-
tag.image = FactoryGirl.create(:docker_image)
|
13
|
-
|
14
|
-
assert tag.valid?
|
15
|
-
assert tag.save
|
16
|
-
end
|
17
|
-
end
|