mirror of
https://github.com/semaphoreui/semaphore.git
synced 2025-01-20 15:29:28 +01:00
commit
0cc72360be
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
FROM iojs:onbuild
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
|
||||||
|
ADD . /srv/semaphore
|
||||||
|
WORKDIR /srv/semaphore
|
||||||
|
|
||||||
|
RUN rm -f node_modules/
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
CMD ["node", "/srv/semaphore/bin/semaphore"]
|
||||||
|
|
||||||
|
EXPOSE 80
|
100
README.md
100
README.md
@ -1,49 +1,81 @@
|
|||||||
semaphore
|
# semaphore
|
||||||
=========
|
|
||||||
|
|
||||||
Open Source Alternative to Ansible Tower
|
Open Source Alternative to Ansible Tower
|
||||||
|
|
||||||
![](public/img/screenshot.png)
|
![screenshot](public/img/screenshot.png)
|
||||||
|
|
||||||
Features
|
## Features
|
||||||
--------
|
|
||||||
|
|
||||||
The basics of Ansible Tower, but in addition:
|
The basics of Ansible Tower, but in addition:
|
||||||
|
|
||||||
- Fast, Simple interface that doesn’t get in the way
|
- [x] Fast, Simple interface (not having to submit a million forms to get something simple done)
|
||||||
- Task output is streamed live via websocket
|
- [x] Task output is streamed live via websocket
|
||||||
- Free. MIT Licensed. Do what you want.
|
- [x] Create inventories per playbook
|
||||||
|
- [x] Add rsa keys (to authenticate git repositories)
|
||||||
|
- [x] Run playbooks against specified hosts
|
||||||
|
- [ ] Multiple Users support
|
||||||
|
|
||||||
How to run:
|
## Docker quickstart
|
||||||
-----------
|
|
||||||
|
|
||||||
1. Install Vagrant
|
### Run redis
|
||||||
2. Run `vagrant up`
|
|
||||||
3. Open [localhost:3000](http://localhost:3000)
|
|
||||||
|
|
||||||
Development steps:
|
```
|
||||||
|
docker run -d \
|
||||||
|
--name=redisio \
|
||||||
|
--restart=always \
|
||||||
|
-v /var/lib/redisio:/var/lib/redis \
|
||||||
|
-p 127.0.0.1:6379:6379 \
|
||||||
|
castawaylabs/redis-docker
|
||||||
|
```
|
||||||
|
|
||||||
Install requirements:
|
### Run mongodb
|
||||||
- node.js >= 0.11.x
|
|
||||||
- an isolated environment (e.g. Docker / NodeGear)
|
|
||||||
- ansible (the tool)
|
|
||||||
- mongodb & redis
|
|
||||||
- Sudo access (this might change). To run jobs, this tool writes private keys to /root/.ssh and copies playbook directories to /root/.
|
|
||||||
|
|
||||||
1. Copy `lib/credentials.default.json` to `lib/credentials.json` and customise, or export relevant environment variables
|
```
|
||||||
2. `bower install`
|
docker run -d \
|
||||||
3. `node bin/semaphore`
|
--name=mongodb \
|
||||||
|
--restart=always \
|
||||||
|
-v /var/lib/mongodb:/var/lib/mongodb \
|
||||||
|
-p 127.0.0.1:27017:27017 \
|
||||||
|
castawaylabs/mongodb-docker
|
||||||
|
```
|
||||||
|
|
||||||
Initial Login
|
### Run semaphore
|
||||||
-------------
|
|
||||||
|
```
|
||||||
|
docker run -d \
|
||||||
|
--name=semaphore \
|
||||||
|
--restart=always \
|
||||||
|
--link redisio:redis \
|
||||||
|
--link mongodb:mongo \
|
||||||
|
-e MONGODB_URL="mongodb://mongo/semaphore" \
|
||||||
|
-e REDIS_HOST="redis" \
|
||||||
|
-p 80:80 \
|
||||||
|
castawaylabs/semaphore
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
1. Install VirtualBox & Vagrant
|
||||||
|
2. Run `vagrant plugin install gatling-rsync-auto`
|
||||||
|
3. Run `vagrant up` to start the vagrant box
|
||||||
|
4. Run `vagrant gatling-rsync-auto` to synchronise changes from your local machine to vagrant
|
||||||
|
|
||||||
|
### Running semaphore inside vagrant
|
||||||
|
|
||||||
|
1. `vagrant ssh`, `cd /opt/semaphore`
|
||||||
|
2. `npm install`
|
||||||
|
3. `bower install`
|
||||||
|
4. `npm install -g nodemon`
|
||||||
|
5. `nodemon bin/semaphore`
|
||||||
|
|
||||||
|
## Initial Login
|
||||||
|
|
||||||
```
|
```
|
||||||
Email: 'admin@semaphore.local'
|
Email: 'admin@semaphore.local'
|
||||||
Password: 'CastawayLabs'
|
Password: 'CastawayLabs'
|
||||||
```
|
```
|
||||||
|
|
||||||
Environment Variables
|
## Environment Variables
|
||||||
---------------------
|
|
||||||
|
|
||||||
Use these variables to override the config.
|
Use these variables to override the config.
|
||||||
|
|
||||||
@ -58,9 +90,19 @@ Use these variables to override the config.
|
|||||||
| SMTP_PASS | Mandrill smtp password | |
|
| SMTP_PASS | Mandrill smtp password | |
|
||||||
| MONGODB_URL | Mongodb URL | `mongodb://127.0.0.1/semaphore` |
|
| MONGODB_URL | Mongodb URL | `mongodb://127.0.0.1/semaphore` |
|
||||||
|
|
||||||
Note to Ansible guys
|
## Vision and goals for v1
|
||||||
--------------------
|
|
||||||
|
- Be able to specify environment information per playbook / per task
|
||||||
|
- Schedule jobs
|
||||||
|
- Email alerts
|
||||||
|
- Multiple user support
|
||||||
|
|
||||||
|
## Note to Ansible guys
|
||||||
|
|
||||||
> Thanks very much for making Ansible, and Ansible Tower. It is a great tool!. Your UI is pretty horrible though, and so we'd be happy if you could learn and use parts of this tool in your Tower.
|
> Thanks very much for making Ansible, and Ansible Tower. It is a great tool!. Your UI is pretty horrible though, and so we'd be happy if you could learn and use parts of this tool in your Tower.
|
||||||
|
|
||||||
It would be amazing if this could be your `Community Edition` of Ansible Tower.
|
It would be amazing if this could be your `Community Edition` of Ansible Tower.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
@ -13,4 +13,4 @@
|
|||||||
"angular-couch-potato": "~0.1.1",
|
"angular-couch-potato": "~0.1.1",
|
||||||
"angular-ui-router": "~0.2.10"
|
"angular-ui-router": "~0.2.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,6 +4,9 @@ var express = require('express')
|
|||||||
|
|
||||||
exports.unauthorized = function (app, template) {
|
exports.unauthorized = function (app, template) {
|
||||||
template([
|
template([
|
||||||
|
'tasks',
|
||||||
|
'jobs',
|
||||||
|
'hosts',
|
||||||
'view'
|
'view'
|
||||||
], {
|
], {
|
||||||
prefix: 'playbook'
|
prefix: 'playbook'
|
||||||
|
@ -27,19 +27,12 @@ html
|
|||||||
li: a(ui-sref="addPlaybook") Add Playbook
|
li: a(ui-sref="addPlaybook") Add Playbook
|
||||||
|
|
||||||
.container-fluid
|
.container-fluid
|
||||||
.row
|
.col-lg-12
|
||||||
.col-sm-3.col-lg-2
|
block content
|
||||||
ul.nav
|
ui-view(autoscroll="false")
|
||||||
h2.no-top-margin Playbooks
|
p.lead.text-center
|
||||||
li(ng-repeat="playbook in playbooks")
|
i.fa.fa-spin.fa-cog
|
||||||
a(ui-sref="playbook.view({ playbook_id: playbook._id })") {{ playbook.name }}
|
| Loading...
|
||||||
|
|
||||||
.col-sm-9.col-lg-10
|
|
||||||
block content
|
|
||||||
ui-view(autoscroll="false")
|
|
||||||
p.lead.text-center
|
|
||||||
i.fa.fa-spin.fa-cog
|
|
||||||
| Loading...
|
|
||||||
|
|
||||||
block js
|
block js
|
||||||
script(src="/vendor/requirejs/require.js" data-main="/js/semaphore.js")
|
script(src="/vendor/requirejs/require.js" data-main="/js/semaphore.js")
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
h1 Add Playbook
|
h1
|
||||||
|
span(ng-if="!playbook.data._id") Add Playbook
|
||||||
|
span(ng-if="playbook.data._id.length > 0") Edit Playbook
|
||||||
|
|
||||||
.row
|
.row
|
||||||
.col-md-6
|
div(ng-class="{ 'col-md-6': !playbook.data._id, 'col-md-12': playbook.data._id.length > 0 }")
|
||||||
.panel.panel-default
|
.panel.panel-default
|
||||||
.panel-body
|
.panel-body
|
||||||
form.form-horizontal(name="playbookForm")
|
form.form-horizontal(name="playbookForm")
|
||||||
@ -31,5 +33,6 @@ h1 Add Playbook
|
|||||||
button.btn.btn-success(ng-click="add()" ng-disabled="playbookForm.$invalid")
|
button.btn.btn-success(ng-click="add()" ng-disabled="playbookForm.$invalid")
|
||||||
span(ng-if="!playbook.data._id") Add
|
span(ng-if="!playbook.data._id") Add
|
||||||
span(ng-if="playbook.data._id.length > 0") Edit
|
span(ng-if="playbook.data._id.length > 0") Edit
|
||||||
.col-md-6
|
a.btn.btn-danger(ng-if="playbook.data._id.length > 0" ng-click="delete()" style="margin-left: 10px;") Delete
|
||||||
|
.col-md-6(ng-if="!playbook.data._id")
|
||||||
p.lead It is recommended you create an identity before creating playbooks.
|
p.lead It is recommended you create an identity before creating playbooks.
|
@ -1,4 +1,4 @@
|
|||||||
h2 Hosts
|
h1 Hosts
|
||||||
button.btn.btn-default.pull-right(ng-click="add()") Add Group
|
button.btn.btn-default.pull-right(ng-click="add()") Add Group
|
||||||
|
|
||||||
div(ng-repeat="hostgroup in hostgroups.hostgroups")
|
div(ng-repeat="hostgroup in hostgroups.hostgroups")
|
@ -1,4 +1,4 @@
|
|||||||
h2 Jobs
|
h1 Jobs
|
||||||
button.btn.btn-default.pull-right(ng-click="add()") Add Job
|
button.btn.btn-default.pull-right(ng-click="add()") Add Job
|
||||||
|
|
||||||
table.table.table-hover
|
table.table.table-hover
|
@ -5,6 +5,6 @@ h1 Playbooks
|
|||||||
|
|
||||||
table.table
|
table.table
|
||||||
tr(ng-repeat="playbook in playbooks")
|
tr(ng-repeat="playbook in playbooks")
|
||||||
td: a(ui-sref="playbook.view({ playbook_id: playbook._id })") {{ playbook.name }}
|
td: a(ui-sref="playbook.tasks({ playbook_id: playbook._id })") {{ playbook.name }}
|
||||||
|
|
||||||
blockquote Playbooks are ansible playbooks. Each playbook defines a git repository with the playbook code. Semaphore downloads the playbook and runs the task file.
|
blockquote Playbooks are ansible playbooks. Each playbook defines a git repository with the playbook code. Semaphore downloads the playbook and runs the task file.
|
@ -1,7 +1,7 @@
|
|||||||
h2 Tasks
|
h1(ng-if="tasks.tasks.length") Tasks
|
||||||
small(ng-if="status.length > 0" ng-bind="status")
|
small(ng-if="status.length > 0" ng-bind="status")
|
||||||
|
|
||||||
table.table.table-hover
|
table.table.table-hover(ng-if="tasks.tasks.length")
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
th Job Name
|
th Job Name
|
||||||
@ -33,4 +33,8 @@ table.table.table-hover
|
|||||||
button(data-dismiss="modal").close: span ×
|
button(data-dismiss="modal").close: span ×
|
||||||
h4.modal-title Task Output
|
h4.modal-title Task Output
|
||||||
.modal-body
|
.modal-body
|
||||||
pre: code {{ openTask.data.output }}
|
pre: code {{ openTask.data.output }}
|
||||||
|
|
||||||
|
h4.text-center.text-muted(ng-if="!tasks.tasks.length" style="margin-top: 15px;") It seems no
|
||||||
|
a(ui-sref="playbook.jobs") jobs
|
||||||
|
| have been run yet.
|
@ -1,16 +1,15 @@
|
|||||||
h1 {{ playbook.data.name }}
|
mixin menuItem(state, name)
|
||||||
.btn-group.pull-right
|
li(ng-class="{ active: $state.includes('#{state}') }"): a(ui-sref=state+"({ playbook_id: playbook.data._id })")= name
|
||||||
button.btn.btn-success(ui-sref="playbook.edit({ playbook_id: playbook.data._id })") Edit
|
|
||||||
button.btn.btn-danger(ng-click="delete()") Delete
|
|
||||||
|
|
||||||
hr
|
.row
|
||||||
|
.col-sm-3.col-lg-2
|
||||||
ui-view(name="tasks")
|
ul.nav
|
||||||
|
h2.no-top-margin {{ playbook.data.name }}
|
||||||
hr
|
+menuItem("playbook.tasks", "Tasks")
|
||||||
|
+menuItem("playbook.jobs", "Jobs")
|
||||||
ui-view(name="jobs")
|
+menuItem("playbook.hosts", "Hosts")
|
||||||
|
br
|
||||||
hr
|
ul.nav
|
||||||
|
li: a(ui-sref="playbook.edit") Edit
|
||||||
ui-view(name="hosts")
|
.col-sm-9.col-lg-10
|
||||||
|
ui-view
|
@ -22,7 +22,6 @@
|
|||||||
- runit
|
- runit
|
||||||
- npm: name={{ item }} global=yes
|
- npm: name={{ item }} global=yes
|
||||||
with_items:
|
with_items:
|
||||||
- grunt-cli
|
|
||||||
- bower
|
- bower
|
||||||
|
|
||||||
# source is copied using ansible.
|
# source is copied using ansible.
|
||||||
|
@ -28,4 +28,10 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
&.no-top-margin {
|
&.no-top-margin {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.nav {
|
||||||
|
& > li.active a {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
}
|
}
|
@ -18,7 +18,7 @@ define([
|
|||||||
$scope.playbook.add()
|
$scope.playbook.add()
|
||||||
.success(function (data) {
|
.success(function (data) {
|
||||||
playbooks.getPlaybooks(function () {
|
playbooks.getPlaybooks(function () {
|
||||||
$state.transitionTo('playbook.view', {
|
$state.transitionTo('playbook.tasks', {
|
||||||
playbook_id: data._id
|
playbook_id: data._id
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -5,10 +5,10 @@ define([
|
|||||||
'factories/hostgroup',
|
'factories/hostgroup',
|
||||||
'factories/host'
|
'factories/host'
|
||||||
], function(app, $) {
|
], function(app, $) {
|
||||||
app.registerController('HostsCtrl', ['$scope', '$state', 'hostgroups', 'Host', function($scope, $state, hostgroups, Host) {
|
app.registerController('PlaybookHostsCtrl', ['$scope', '$state', 'hostgroups', 'Host', function($scope, $state, hostgroups, Host) {
|
||||||
|
|
||||||
$scope.hostgroups = hostgroups;
|
$scope.hostgroups = hostgroups;
|
||||||
|
|
||||||
hostgroups.get($scope.playbook, function () {
|
hostgroups.get($scope.playbook, function () {
|
||||||
});
|
});
|
||||||
|
|
@ -4,9 +4,9 @@ define([
|
|||||||
'services/jobs',
|
'services/jobs',
|
||||||
'factories/job'
|
'factories/job'
|
||||||
], function(app, $) {
|
], function(app, $) {
|
||||||
app.registerController('JobsCtrl', ['$scope', 'jobs', function($scope, jobs) {
|
app.registerController('PlaybookJobsCtrl', ['$scope', 'jobs', function($scope, jobs) {
|
||||||
$scope.jobs = jobs;
|
$scope.jobs = jobs;
|
||||||
|
|
||||||
jobs.get($scope.playbook, function () {
|
jobs.get($scope.playbook, function () {
|
||||||
});
|
});
|
||||||
|
|
@ -7,9 +7,9 @@ define([
|
|||||||
], function(app, $, io) {
|
], function(app, $, io) {
|
||||||
var socket = io();
|
var socket = io();
|
||||||
|
|
||||||
app.registerController('TasksCtrl', ['$scope', 'tasks', 'Task', function($scope, tasks, Task) {
|
app.registerController('PlaybookTasksCtrl', ['$scope', 'tasks', 'Task', function($scope, tasks, Task) {
|
||||||
$scope.tasks = tasks;
|
$scope.tasks = tasks;
|
||||||
|
|
||||||
tasks.get($scope.playbook, function () {
|
tasks.get($scope.playbook, function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ define([
|
|||||||
if (!task.data.output) {
|
if (!task.data.output) {
|
||||||
task.data.output = "";
|
task.data.output = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
task.data.output += data.output;
|
task.data.output += data.output;
|
||||||
|
|
||||||
if (!$scope.$$phase) {
|
if (!$scope.$$phase) {
|
@ -30,10 +30,7 @@ define([
|
|||||||
controller: 'PlaybookCtrl',
|
controller: 'PlaybookCtrl',
|
||||||
templateUrl: '/view/playbook/view',
|
templateUrl: '/view/playbook/view',
|
||||||
resolve: {
|
resolve: {
|
||||||
dummy: $couchPotatoProvider.resolve(['controllers/playbook/playbook',
|
dummy: $couchPotatoProvider.resolve(['controllers/playbook/playbook']),
|
||||||
'controllers/host/hosts',
|
|
||||||
'controllers/job/jobs',
|
|
||||||
'controllers/task/tasks']),
|
|
||||||
playbook: function (Playbook, $stateParams, $q, $state) {
|
playbook: function (Playbook, $stateParams, $q, $state) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
@ -51,36 +48,37 @@ define([
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
.state('playbook.view', {
|
|
||||||
url: '',
|
|
||||||
views: {
|
|
||||||
tasks: {
|
|
||||||
templateUrl: '/view/task/tasks',
|
|
||||||
controller: 'TasksCtrl'
|
|
||||||
},
|
|
||||||
jobs: {
|
|
||||||
templateUrl: '/view/job/jobs',
|
|
||||||
controller: 'JobsCtrl'
|
|
||||||
},
|
|
||||||
hosts: {
|
|
||||||
templateUrl: '/view/host/hosts',
|
|
||||||
controller: 'HostsCtrl'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
.state('playbook.edit', {
|
.state('playbook.edit', {
|
||||||
url: '/edit',
|
url: '/edit',
|
||||||
templateUrl: "/view/playbook/add",
|
templateUrl: "/view/playbook/add",
|
||||||
controller: 'EditPlaybookCtrl',
|
controller: 'EditPlaybookCtrl',
|
||||||
resolve: {
|
resolve: {
|
||||||
dummy: $couchPotatoProvider.resolve(['controllers/playbook/edit'])
|
dummy: $couchPotatoProvider.resolve(['controllers/playbook/edit'])
|
||||||
},
|
}
|
||||||
views: {
|
})
|
||||||
tasks: {
|
|
||||||
templateUrl: '/view/playbook/add',
|
.state('playbook.tasks', {
|
||||||
controller: 'EditPlaybookCtrl'
|
url: '/tasks',
|
||||||
}
|
templateUrl: "/view/playbook/tasks",
|
||||||
|
controller: 'PlaybookTasksCtrl',
|
||||||
|
resolve: {
|
||||||
|
dummy: $couchPotatoProvider.resolve(['controllers/playbook/tasks'])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.state('playbook.jobs', {
|
||||||
|
url: '/jobs',
|
||||||
|
templateUrl: "/view/playbook/jobs",
|
||||||
|
controller: 'PlaybookJobsCtrl',
|
||||||
|
resolve: {
|
||||||
|
dummy: $couchPotatoProvider.resolve(['controllers/playbook/jobs'])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.state('playbook.hosts', {
|
||||||
|
url: '/hosts',
|
||||||
|
templateUrl: "/view/playbook/hosts",
|
||||||
|
controller: 'PlaybookHostsCtrl',
|
||||||
|
resolve: {
|
||||||
|
dummy: $couchPotatoProvider.resolve(['controllers/playbook/hosts'])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1 +0,0 @@
|
|||||||
vagrant ssh -- -R 27017:localhost:27017 -R 6379:localhost:6379 -R 3000:localhost:3000
|
|
Loading…
Reference in New Issue
Block a user