magellan-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +60 -0
- data/LICENSE.txt +22 -0
- data/README.md +109 -0
- data/Rakefile +7 -0
- data/bin/magellan-cli +13 -0
- data/launch_options.json +7 -0
- data/lib/magellan/cli/base.rb +50 -0
- data/lib/magellan/cli/command.rb +29 -0
- data/lib/magellan/cli/direct.rb +32 -0
- data/lib/magellan/cli/errors.rb +14 -0
- data/lib/magellan/cli/http.rb +103 -0
- data/lib/magellan/cli/login.rb +134 -0
- data/lib/magellan/cli/resources/base.rb +136 -0
- data/lib/magellan/cli/resources/client_version.rb +15 -0
- data/lib/magellan/cli/resources/container_image.rb +13 -0
- data/lib/magellan/cli/resources/container_instance.rb +25 -0
- data/lib/magellan/cli/resources/function_unit.rb +27 -0
- data/lib/magellan/cli/resources/host_instance.rb +56 -0
- data/lib/magellan/cli/resources/project.rb +22 -0
- data/lib/magellan/cli/resources/stage.rb +69 -0
- data/lib/magellan/cli/resources/worker_version.rb +41 -0
- data/lib/magellan/cli/resources.rb +27 -0
- data/lib/magellan/cli/sample_launch_options.json +7 -0
- data/lib/magellan/cli/ssl.rb +35 -0
- data/lib/magellan/cli/version.rb +5 -0
- data/lib/magellan/cli.rb +23 -0
- data/magellan-cli.gemspec +29 -0
- data/spec/magellan/cli/Magellan.yml +29 -0
- data/spec/magellan/cli/login_page.html +42 -0
- data/spec/magellan/cli/login_spec.rb +46 -0
- data/spec/magellan/cli/resources/project_spec.rb +39 -0
- data/spec/magellan/cli_spec.rb +7 -0
- data/spec/spec_helper.rb +2 -0
- metadata +187 -0
@@ -0,0 +1,134 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli"
|
3
|
+
|
4
|
+
require 'httpclient'
|
5
|
+
require 'json'
|
6
|
+
require 'uri'
|
7
|
+
require 'nokogiri'
|
8
|
+
|
9
|
+
require 'active_support/core_ext/hash/keys'
|
10
|
+
|
11
|
+
module Magellan
|
12
|
+
module Cli
|
13
|
+
class Login
|
14
|
+
|
15
|
+
PRODUCTION_HTTP_PORT = 80
|
16
|
+
PRODUCTION_HTTPS_PORT = 443
|
17
|
+
|
18
|
+
DEFAULT_HTTP_PORT = (ENV['DEFAULT_HTTP_PORT' ] || 80).to_i
|
19
|
+
DEFAULT_HTTPS_PORT = (ENV['DEFAULT_HTTPS_PORT'] || 443).to_i
|
20
|
+
|
21
|
+
attr_reader :httpclient
|
22
|
+
attr_reader :base_http_url
|
23
|
+
attr_reader :base_https_url
|
24
|
+
attr_reader :ssl_disabled
|
25
|
+
|
26
|
+
attr_reader :auth_token
|
27
|
+
|
28
|
+
# Magellan::Cli::Loginのコンストラクタです。
|
29
|
+
#
|
30
|
+
# @param [String] base_url_or_host 接続先の基準となるURLあるいはホスト名
|
31
|
+
# @param [Hash] options オプション
|
32
|
+
# @option options [String] :api_version APIのバージョン。デフォルトは "1.0.0"
|
33
|
+
# @option options [Boolean] :ssl_disabled SSLを無効にするかどうか。
|
34
|
+
# @option options [Integer] :https_port HTTPSで接続する際の接続先のポート番号
|
35
|
+
def initialize(base_url_or_host = nil, options = {})
|
36
|
+
base_url_or_host ||= (ENV["MAGELLAN_SITE"] || "http://localhost:3000/")
|
37
|
+
if base_url_or_host =~ URI.regexp
|
38
|
+
@base_http_url = base_url_or_host.sub(/\/\Z/, '')
|
39
|
+
uri = URI.parse(@base_http_url)
|
40
|
+
else
|
41
|
+
if config_path = search_file(".magellan-cli.yml")
|
42
|
+
config = YAML.load_file_with_erb(config_path)
|
43
|
+
options = config[base_url_or_host.to_s].deep_symbolize_keys.update(options)
|
44
|
+
end
|
45
|
+
uri = URI::Generic.build({scheme: "http", host: base_url_or_host, port: DEFAULT_HTTP_PORT}.update(options))
|
46
|
+
@base_http_url = uri.to_s
|
47
|
+
end
|
48
|
+
@ssl_disabled = options.delete(:ssl_disabled)
|
49
|
+
@ssl_disabled ||= (uri.port == 3000)
|
50
|
+
@base_https_url = @ssl_disabled ? @base_http_url : build_https_url(uri, options[:https_port])
|
51
|
+
# @api_version = options[:api_version] || "1.0.0"
|
52
|
+
|
53
|
+
@httpclient = HTTPClient.new
|
54
|
+
@httpclient.ssl_config.verify_mode = nil # 自己署名の証明書をOKにする
|
55
|
+
end
|
56
|
+
|
57
|
+
def build_https_url(uri, port = nil)
|
58
|
+
uri.scheme = "https"
|
59
|
+
uri.port = port || (uri.port == PRODUCTION_HTTP_PORT ? PRODUCTION_HTTPS_PORT : uri.port + 1)
|
60
|
+
uri.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
def login_form_url
|
64
|
+
@login_form_url ||= base_https_url + "/users/sign_in.html"
|
65
|
+
end
|
66
|
+
def login_url
|
67
|
+
@login_url ||= base_https_url + "/api/sign_in.json"
|
68
|
+
end
|
69
|
+
|
70
|
+
# magellan-apiサーバに接続してログインの検証と処理を行います。
|
71
|
+
#
|
72
|
+
# @param [Hash] extra オプション
|
73
|
+
# @return [Integer] サーバが返したレスポンスのステータスを返します
|
74
|
+
# @return [String] サーバが返したレスポンスのbodyを返します
|
75
|
+
def login_and_status
|
76
|
+
@auth_token ||= get_auth_token
|
77
|
+
params = {
|
78
|
+
"user" => {"email" => "magellan@groovenauts.jp", "password" => "password"},
|
79
|
+
"authenticity_token" => @auth_token
|
80
|
+
}.to_json
|
81
|
+
res2 = Ssl.retry_on_ssl_error("login"){ @httpclient.post(login_url, params, JSON_HEADER) }
|
82
|
+
return res2.status, res2.body
|
83
|
+
end
|
84
|
+
|
85
|
+
# GSSサーバに接続してログインの検証と処理を行います。
|
86
|
+
#
|
87
|
+
# @param [Hash] extra オプション
|
88
|
+
# @option extra [Integer] :device_type デバイス種別
|
89
|
+
# @option extra [Integer] :device_id デバイス識別子
|
90
|
+
# @return [Boolean] ログインに成功した場合はtrue、失敗した場合はfalse
|
91
|
+
def login
|
92
|
+
status, _ = login_and_status
|
93
|
+
case status
|
94
|
+
when 200...300 then true
|
95
|
+
else false
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# GSSサーバに接続してログインの検証と処理を行います。
|
100
|
+
#
|
101
|
+
# @param [Hash] extra オプション
|
102
|
+
# @see #login
|
103
|
+
# @return ログインに成功した場合は自身のオブジェクト返します。失敗した場合はLibgss::Network::Errorがraiseされます。
|
104
|
+
def login!
|
105
|
+
status, body = login_and_status
|
106
|
+
case status
|
107
|
+
when 200...300 then return self
|
108
|
+
else raise LoginError, "status: #{status} #{body}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_auth_token
|
113
|
+
res = Ssl.retry_on_ssl_error("login_form"){ @httpclient.get(login_form_url) }
|
114
|
+
doc = Nokogiri::HTML.parse(res.body, login_form_url, res.body_encoding.to_s)
|
115
|
+
node = doc.xpath('//input[@name="authenticity_token"]').first
|
116
|
+
node.attribute('value').value
|
117
|
+
end
|
118
|
+
|
119
|
+
# @httpclient.inspectの戻り値の文字列が巨大なので、inspectで出力しないようにします。
|
120
|
+
def inspect
|
121
|
+
r = "#<#{self.class.name}:#{self.object_id} "
|
122
|
+
fields = (instance_variables - [:@httpclient]).map{|f| "#{f}=#{instance_variable_get(f).inspect}"}
|
123
|
+
r << fields.join(", ") << ">"
|
124
|
+
end
|
125
|
+
|
126
|
+
def search_file(basename)
|
127
|
+
dirs = [".", "./config", ENV['HOME']].join(",")
|
128
|
+
Dir["{#{dirs}}/#{basename}"].select{|path| File.readable?(path)}.first
|
129
|
+
end
|
130
|
+
private :search_file
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'yaml'
|
6
|
+
require 'active_support/core_ext/string/inflections'
|
7
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
8
|
+
require 'active_support/core_ext/object/blank'
|
9
|
+
|
10
|
+
module Magellan
|
11
|
+
module Cli
|
12
|
+
module Resources
|
13
|
+
|
14
|
+
class NotSelected < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
class NotFound < StandardError
|
18
|
+
end
|
19
|
+
|
20
|
+
class Base < ::Magellan::Cli::Http
|
21
|
+
|
22
|
+
no_commands do
|
23
|
+
|
24
|
+
def load_selections
|
25
|
+
File.readable?(".magellan-cli") ? YAML.load_file(".magellan-cli") : {}
|
26
|
+
end
|
27
|
+
|
28
|
+
def load_selection(name)
|
29
|
+
sel = load_selections
|
30
|
+
s = sel[name]
|
31
|
+
raise NotSelected, "No #{name} selected" unless s
|
32
|
+
return s
|
33
|
+
end
|
34
|
+
|
35
|
+
def update_selections(hash = nil)
|
36
|
+
sel = load_selections
|
37
|
+
sel.update(hash) if hash
|
38
|
+
yield(sel) if block_given?
|
39
|
+
open(".magellan-cli", "w") do |f|
|
40
|
+
YAML.dump(sel, f)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_query(hash)
|
45
|
+
@@no ||= 0
|
46
|
+
r = {}
|
47
|
+
hash.each do |key, value|
|
48
|
+
@@no += 1
|
49
|
+
r["f[#{key}][#{@@no}][o]"] = "is"
|
50
|
+
r["f[#{key}][#{@@no}][v]"] = value
|
51
|
+
end
|
52
|
+
return r
|
53
|
+
end
|
54
|
+
|
55
|
+
def default_query
|
56
|
+
sel = load_selections
|
57
|
+
q = {}
|
58
|
+
(self.class.resource_dependency || {}).each do |f, res|
|
59
|
+
r = sel[res]
|
60
|
+
raise NotSelected, "No #{f} selected" unless r
|
61
|
+
q[f] = r["id"]
|
62
|
+
end
|
63
|
+
return build_query(q)
|
64
|
+
end
|
65
|
+
|
66
|
+
DEFAULT_SELECTION_FIELDS = %w[id name].freeze
|
67
|
+
|
68
|
+
def update_first_result(name, path, query, fields = DEFAULT_SELECTION_FIELDS)
|
69
|
+
results = get_json(path, query)
|
70
|
+
raise NotFound, "#{name} not found for #{query.inspect}" if results.blank? || results.first.blank?
|
71
|
+
r = results.first
|
72
|
+
obj = fields.each_with_object({}) do |f, d|
|
73
|
+
d[f] = r[f]
|
74
|
+
end
|
75
|
+
update_selections(name => obj)
|
76
|
+
return r
|
77
|
+
end
|
78
|
+
|
79
|
+
def list
|
80
|
+
r = get_json("/admin/#{self.class.resource_name}.json", default_query)
|
81
|
+
$stdout.puts(JSON.pretty_generate(r) << "\nTotal: #{r.length}")
|
82
|
+
end
|
83
|
+
|
84
|
+
def show(id)
|
85
|
+
r = get_json("/admin/#{self.class.resource_name}/#{id}.json")
|
86
|
+
$stdout.puts(JSON.pretty_generate(r))
|
87
|
+
end
|
88
|
+
|
89
|
+
def select(name)
|
90
|
+
q = build_query("name" => name).update(default_query)
|
91
|
+
update_first_result(self.class.resource_name, "/admin/#{self.class.resource_name}.json", q)
|
92
|
+
end
|
93
|
+
|
94
|
+
def deselect
|
95
|
+
update_selections do |s|
|
96
|
+
s.delete(self.class.resource_name)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.inherited(klass)
|
101
|
+
base_name = klass.name.split(/::/).last
|
102
|
+
res_name = base_name.underscore
|
103
|
+
klass.module_eval <<-EOM
|
104
|
+
no_commands do
|
105
|
+
cattr_accessor :resource_name
|
106
|
+
cattr_accessor :resource_dependency
|
107
|
+
end
|
108
|
+
|
109
|
+
desc "list", "list #{base_name}"
|
110
|
+
def list
|
111
|
+
super
|
112
|
+
end
|
113
|
+
|
114
|
+
desc "show ID", "show #{base_name} specified by ID"
|
115
|
+
def show(id)
|
116
|
+
super(id)
|
117
|
+
end
|
118
|
+
|
119
|
+
desc "select NAME", "select #{base_name} by NAME"
|
120
|
+
def select(name)
|
121
|
+
super
|
122
|
+
end
|
123
|
+
|
124
|
+
desc "deselect", "deselect #{base_name}"
|
125
|
+
def deselect
|
126
|
+
super
|
127
|
+
end
|
128
|
+
EOM
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
|
4
|
+
module Magellan
|
5
|
+
module Cli
|
6
|
+
module Resources
|
7
|
+
|
8
|
+
class ClientVersion < Base
|
9
|
+
self.resource_name = "client_version"
|
10
|
+
self.resource_dependency = {"stage" => "stage"}
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
|
4
|
+
module Magellan
|
5
|
+
module Cli
|
6
|
+
module Resources
|
7
|
+
|
8
|
+
class ContainerInstance < Base
|
9
|
+
self.resource_name = "container_instance"
|
10
|
+
self.resource_dependency = {"host" => "host_instance"}
|
11
|
+
|
12
|
+
desc "start ID", "start container instance specified by ID"
|
13
|
+
def start(id)
|
14
|
+
post_json("/admin/container_instance/#{id}/status_control.json", {})
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "stop ID", "stop container instance specified by ID"
|
18
|
+
def stop(id)
|
19
|
+
delete("/admin/container_instance/#{id}/status_control.json")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
|
4
|
+
module Magellan
|
5
|
+
module Cli
|
6
|
+
module Resources
|
7
|
+
|
8
|
+
class FunctionUnit < Base
|
9
|
+
self.resource_name = "function_unit"
|
10
|
+
self.resource_dependency = {"stage" => "stage-version"}
|
11
|
+
|
12
|
+
desc "create [ATTRIBUTES]", "create FunctionUnit with ATTRIBUTES (filepath or JSON text)"
|
13
|
+
def create(attrs = nil)
|
14
|
+
stage_id = load_selection("stage-version")
|
15
|
+
attrs = JSON.parse(File.readable?(attrs) ? File.read(attrs) : attrs)
|
16
|
+
params = {
|
17
|
+
"function_unit" => {
|
18
|
+
"stage_version_id" => stage_id,
|
19
|
+
}.update(attrs)
|
20
|
+
}
|
21
|
+
post_json("/admin/function_unit/new.js", params)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
|
4
|
+
module Magellan
|
5
|
+
module Cli
|
6
|
+
module Resources
|
7
|
+
|
8
|
+
class HostInstance < Base
|
9
|
+
self.resource_name = "host_instance"
|
10
|
+
self.resource_dependency = {"stage" => "stage-version"}
|
11
|
+
|
12
|
+
desc "sample_launch_options", "dump sample launch_options for create"
|
13
|
+
def sample_launch_options
|
14
|
+
$stdout.puts File.read(File.expand_path("../sample_launch_options.json", __FILE__))
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "create NAME [LAUNCH_OPTIONS]", "create Host Instance"
|
18
|
+
def create(name, launch_options = nil)
|
19
|
+
stage_id = load_selection("stage-version")
|
20
|
+
launch_options = JSON.parse(File.readable?(launch_options) ? File.read(launch_options) : launch_options)
|
21
|
+
|
22
|
+
params = {
|
23
|
+
"host_instance" => {
|
24
|
+
"stage_id" => stage_id,
|
25
|
+
"name" => name,
|
26
|
+
"launch_options_yaml" => YAML.dump(launch_options),
|
27
|
+
}
|
28
|
+
}
|
29
|
+
post_json("/admin/host_instance/new.js", params)
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "launch", "launch host instance"
|
33
|
+
def launch
|
34
|
+
id = load_selection(self.class.resource_name)["id"]
|
35
|
+
post_json("/admin/host_instance/#{id}/status_control.json", {})
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "prepare", "prepare host instance"
|
39
|
+
def prepare
|
40
|
+
id = load_selection(self.class.resource_name)["id"]
|
41
|
+
put_json("/admin/host_instance/#{id}/status_control.json", {})
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "stop", "stop host instance"
|
45
|
+
def stop
|
46
|
+
id = load_selection(self.class.resource_name)["id"]
|
47
|
+
delete("/admin/host_instance/#{id}/status_control.json")
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
|
4
|
+
module Magellan
|
5
|
+
module Cli
|
6
|
+
module Resources
|
7
|
+
|
8
|
+
class Project < Base
|
9
|
+
self.resource_name = "project"
|
10
|
+
self.resource_dependency = nil
|
11
|
+
|
12
|
+
desc "update ATTRIBUTES", "update project with ATTRIBUTES"
|
13
|
+
def update(attrs)
|
14
|
+
s = load_selection("project")
|
15
|
+
attrs = JSON.parse(File.readable?(attrs) ? File.read(attrs) : attrs)
|
16
|
+
put_json("/admin/magellan~auth~project/#{s['id']}/edit", {"magellan_auth_project" => attrs})
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "magellan/cli/resources"
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module Magellan
|
6
|
+
module Cli
|
7
|
+
module Resources
|
8
|
+
|
9
|
+
class Stage < Base
|
10
|
+
self.resource_name = "stage~title"
|
11
|
+
self.resource_dependency = {"project" => "project"}
|
12
|
+
|
13
|
+
desc "select NAME", "select Stage named by NAME"
|
14
|
+
def select(name)
|
15
|
+
q = build_query("name" => name).update(default_query)
|
16
|
+
r = update_first_result("stage", "/admin/stage~title.json", q)
|
17
|
+
|
18
|
+
# # current
|
19
|
+
# q = build_query("title" => r["id"], "phase" => 2) # 2: current
|
20
|
+
# update_first_result("stage-version", "/admin/stage~version.json", q, %w[id])
|
21
|
+
|
22
|
+
# # workspace
|
23
|
+
q = build_query("title" => r["id"], "phase" => 1) # 1: workspace
|
24
|
+
update_first_result("stage-version", "/admin/stage~version.json", q, %w[id])
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "planning", "switch to planning to build next released version"
|
28
|
+
def planning
|
29
|
+
switch_version(1)
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "current", "switch to current released version"
|
33
|
+
def current
|
34
|
+
switch_version(2)
|
35
|
+
end
|
36
|
+
|
37
|
+
no_commands do
|
38
|
+
def switch_version(phase)
|
39
|
+
s = load_selection("stage")
|
40
|
+
q = build_query("title" => s["id"], "phase" => phase) # 1: workspace, 2: current, 3: used
|
41
|
+
update_first_result("stage-version", "/admin/stage~version.json", q, %w[id])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "prepare", "prepare containers"
|
46
|
+
def prepare
|
47
|
+
s = load_selection("stage")
|
48
|
+
id = s["id"]
|
49
|
+
post_json("/admin/stage~title/#{id}/simple_method_call.json", {method_name: "prepare_containers"})
|
50
|
+
end
|
51
|
+
|
52
|
+
desc "update ATTRIBUTES", "update stage with ATTRIBUTES"
|
53
|
+
def update(attrs)
|
54
|
+
s = load_selection("stage")
|
55
|
+
attrs = JSON.parse(File.readable?(attrs) ? File.read(attrs) : attrs)
|
56
|
+
put_json("/admin/stage~title/#{s['id']}/edit", {"stage_title" => attrs})
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "release_now", "release changes now"
|
60
|
+
def release_now
|
61
|
+
s = load_selection("stage")
|
62
|
+
id = s["id"]
|
63
|
+
post_json("/admin/stage~title/#{id}/simple_method_call.json", {method_name: "release_now"})
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|