Чтобы Jenkins на нашем сервере смог получить доступ к коду из репозитория — необходимо сгенерировать приватный и публичный ключ и раздать их в соответствии с назначением. Для генерации ключей, в терминате (консоли) сервера вводим команду
$ ssh-keygen
При этом видим предложение сгенерировать ключ по стандартному для этого случая пути
Generating public/private rsa key pair. Enter file in which to save the key (/Users/user01/.ssh/id_rsa):
Рекомендуется выбирать дефолтный путь — жмем Enter, но вы можете и поменять путь.
Далее система попросит ввести passphrase. Для простоты можем оставить это поле пустым, как и следующее (подтверждающее ввод)
Enter passphrase (empty for no passphrase): Enter same passphrase again:
Успешная генерация ключа сопровождается следующим выводом в консоли
Your identification has been saved in /Users/emmap1/.ssh/id_rsa. Your public key has been saved in /Users/emmap1/.ssh/id_rsa.pub. The key fingerprint is: 4c:80:61:2c:00:3f:9d:dc:08:41:2e:c0:cf:b9:17:69 emmap1@myhost.local The key’s randomart image is: +—[ RSA 2048]—-+ |*o+ooo. | |.+.=o+ . | |. *.* o . | | . = E o | | o . S | | . . | | . | | | | | +——————+
Выведем сгенерированные файлы списком на экран
$ ls ~/.ssh
id_rsa id_rsa.pub
Выводится 2 файла:
id_rsa — приватный ключ, который понадобится нам для генерациия пользователя аутентификации в репозитории на cтороне Jenkins
id_rsa.pub — публичный ключ, который необходимо разместить в разделе access keys целевого репозитория
Добавляем публичный ключ в разделе ключей приватного репозитория
Например, в Bitbucket, публичный ключ можно добавить в разделе Repository settings — Access keys — Add key (настройки конкретного репозитория)
Добавляем приватный ключ в Jenkins
Заходим в раздел Управления Ключами (Jenkins — Manage Jenkins — Manage Credentials)
И добавляем приватный ключ (содержимое файла id_rsa.pub) в поле Private key, как проиллюстрировано ниже
Заполняем поле Username — имя для пользователя. Сохраняем.
Осталось подключится к репозиторию.
Перейдем в настройки ранее созданного проекта в Jenkins.
Source Code Management — выбираем Git (1)
Repositories — вставляем ссылку на репозиторий (2) и из выпадающего списка выбираем нашего только что созданного пользователя с приватным ключом (3)
Branches to build — обычно указывают мастер ветку — */master (4)
Сохраняем проект в Jenkins.
Теперь мы готовы собрать проект из приватного репозитория.
Калькулятор хостится на Digital Ocean, сервер на Centos 7, Python + Dash фреймворк для визуализации.
С данным реальным примером, можно начать увлекательное путешествие в мир финансовой грамотности, попутно, ознакомившись с языком Питон, который идеально подходит под задачи автоматизации математических вычислений, а так же фреймворком Даш (на основе более известного фреймворка Flask c элементами ReactJS, но это все под капотом)
Принцип работ инвестиций в дивидендные акции
Один из самых интересных и надежных способов получать пассивный доход — это инвестиции в надежные дивидендные акции.
Наиболее оправданный подход (проверенный годами и многими публичными пассивными инвесторами) — это вкладывать регулярно (например раз в месяц или раз в неделю) одинаковыми суммами в понятные и надежные компании, которые годами наращивают свою выручку и, соответсвенно, дивиденды.
Далее, эти растущие дивиденды ре-инвестируются, что увеличивает регулярные пополнения. Машина раскручивается, и капитал растет.
Все это происходит по экспоненте, как можно убедиться после использования данного калькулятора.
Установка окружения для нашего проекта и размещение на хостинге, публикация.
Подразумевается, что хостинг приобретен, операционная система установлена, веб сервер настроен (или же вы работаете на локальномкомпьютере на Mac или Linux)
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
from app import app
app.layout = html.Div([
html.Label(children="Hello world"),
])
server = app.server
app.config.suppress_callback_exceptions = True
if __name__ == '__main__':
app.run_server(debug=True)
Сохраняем CTRL+O и закрываем редактор CRTL+X
Теперь мы готовы запускать нашу тестовую среду (более подробную документацию на английском языке, если это понадобится, можно найти здесь)
# gunicorn --bind 0.0.0.0:8000 index:server
Наше страничка «Hello world» доступна по адресу
http://{ip_адрес_сервера}:8000
Если успешно запустили, дезактивируем виртуальную среду
# deactivate
и приступаем к настройкам веб-сервера (коим в нашем примере небезызвестный Caddy), чтобы калькулятор был доступен по доменному имени.
Первым делом создаем .service файл в папке /etc/systemd/system
И, последним делом, осталось дать понять, где искать наш сервис веб-серверу. В случае с Caddy это делается максимально просто, плюс с коробки получаем сертификат
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
from app import app
from apps import home, dividends_calculator
app.layout = html.Div([
dcc.Location(id='url', refresh=False),
html.Div(id='page-content')
])
server = app.server
app.config.suppress_callback_exceptions = True
@app.callback(Output('page-content', 'children'),
[Input('url', 'pathname')])
def display_page(pathname):
if pathname == '/':
return home.layout
elif pathname == '/dividends-calculator':
return dividends_calculator.layout
else:
return '404'
if __name__ == '__main__':
app.run_server(debug=True)
# nano app.py
Вставляем код ниже и сохраняем
#from flask import Flask
import dash
import dash_core_components as dcc
import dash_html_components as html
print(dcc.__version__) # 0.6.0 or above is required
external_css = ["https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css",
"https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css",
"https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css",
"https://fonts.googleapis.com/css?family=Raleway:400,300,600",
"https://codepen.io/chriddyp/pen/bWLwgP.css",
"https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"]
app = dash.Dash(external_stylesheets=external_css)
# mkdirs apps && cd apps
# nano __init__.py
Оставляем его пустым и сохраняем.
Далее
# nano commonmodules.py
Вставляем код ниже и сохраняем
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
def get_header():
header = html.Div([
html.Div([
html.H1(
'List of Dashes')
], className="twelve columns padded"),
], className="gs-text-header")
return header
def get_menu():
menu = html.Div([
dcc.Link('Home ', href='/', className="p-2 text-dark"),
dcc.Link('Dividends Calculator ', href='/dividends-calculator', className="p-2 text-dark")
], className="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm")
return menu
# nano home.py
Вставляем код ниже и сохраняем
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
from apps import commonmodules
from app import app
layout = html.Div([
commonmodules.get_menu(),
html.H1('This is home screen'),
html.A('My blog', href='https://questpro.club')
])
# nano dividends_calculator.py
Вставляем код ниже и сохраняем
# -*- coding: utf-8 -*-
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
from apps import commonmodules
from app import app
meta_tags = [
{'name':'description',
'content':'Дивидендный калькулятор с учетом сложного процента'},
{'name':'title',
'content':'Дивидендный калькулятор'}
]
#app.external_stylesheets = external_stylesheets
app.meta_tags = meta_tags
app.title = 'Дивидендный калькулятор'
bottom_text = '''
1) Все суммы в долларах, проценты - в %
2) По умолчанию, средний размер дивидентов при покупке акций устанавливаем в 4%, можно поменять
3) По умолчанию, средний рост цены акций в год устанавливаем в 12.5% на основе статистики по S&P Dividends aristocrats
4) Все дивиденды реинвестируем для осуществления скорейшего роста - сложный процент
'''
default__divident_income_per_month = 1000
default__start_capital = 1000
default__regular_payment = 1000
default__start_divident_percent = 4
default__average_cost_grow_percent = 12.5
layout = html.Div([
commonmodules.get_menu(),
html.H1('Дивидендный калькулятор с учетом сложного процента'),
html.Div([
html.Div([
html.Div([], className="col-sm-1"),
html.Label(children='Желаемый средний доход в месяц по дивидентам ($)', className="col-sm-4 col-form-label"),
html.Div([
dcc.Input(id='divident-income-per-month', value='1000', type='text', className="form-control-plaintext")
], className="col-sm-4")
], className="form-group row"),
html.Div([
html.Div([], className="col-sm-1"),
html.Label(children='Первоначальный взнос ($)', className="col-sm-4 col-form-label"),
html.Div([
dcc.Input(id='start-capital', value='1000', type='text', className="form-control-plaintext")
], className="col-sm-4")
], className="form-group row"),
html.Div([
html.Div([], className="col-sm-1"),
html.Label(children='Частота очередного поступления: 1 месяц', className="col-sm-4 col-form-label")
], className="form-group row"),
html.Div([
html.Div([], className="col-sm-1"),
html.Label(children='Размер очередного поступления ($)', className="col-sm-4 col-form-label"),
html.Div([
dcc.Input(id='regular-payment', value='1000', type='text', className="form-control-plaintext")
], className="col-sm-4")
], className="form-group row"),
html.Div([
html.Div([], className="col-sm-1"),
html.Label(children='Средний размер дивидентов в при покупке акций (%)', className="col-sm-4 col-form-label"),
html.Div([
dcc.Input(id='start-divident-percent', value='4', type='text', className="form-control-plaintext")
], className="col-sm-4")
], className="form-group row"),
html.Div([
html.Div([], className="col-sm-1"),
html.Label(children='Средний рост цены акций в год (%)', className="col-sm-4 col-form-label"),
html.Div([
dcc.Input(id='average-cost-grow-percent', value='12.5', type='text', className="form-control-plaintext")
], className="col-sm-4")
], className="form-group row"),
html.Div([
html.Div([], className="col-sm-1"),
html.Label(children='Отображаем на графиках максимум лет', className="col-sm-4 col-form-label"),
], className="form-group row"),
dcc.Slider(
id='maximum-years',
min=10,
max=50,
marks={i: 'рассматриваем максимум лет {}'.format(i) if i == 1 else str(i) for i in range(5, 51)},
value=11
),
html.Div([
html.Div([], className="col-sm-1"),
html.Label(id='result', className="col-sm-8 col-form-label"),
], className="form-group row")
], className=""),
html.Div([
dcc.Graph(id='dividends-graph'),
dcc.Graph(id='capital-graph')
]),
html.Div([
dcc.Markdown(children=bottom_text)
])
])
def create_dividends_graph(df, target, title):
return {
'data': [dict(
x=df['month'],
y=df['dividend_no_reinv'],
mode='lines',
name='Дивиденды - без реинвестиций и без роста цены акций'
), dict(
x=df['month'],
y=df['dividend_percent_reinv'],
mode='lines',
name='Дивиденды - рост за счет роста акций'
), dict(
x=df['month'],
y=df['dividend_all_reinv'],
mode='lines',
name='Дивиденды - рост цен акций и реинвестирование дивидендов'
), dict(
x=[target['month'],],
y=[target['dividend'],],
mode='marks',
name='Целевое значение дивидендов'
)],
'layout': {
'annotations': [{
'x': 0, 'y': 0, 'xanchor': 'left', 'yanchor': 'bottom',
'xref': 'paper', 'yref': 'paper', 'showarrow': False,
'align': 'left', 'bgcolor': 'rgba(255, 255, 255, 0.5)',
'text': title
}],
'yaxis': {'type': 'linear', 'title': 'Дивиденды в месяц, $'},
'xaxis': {'showgrid': True, 'title': 'Месяцы'}
}
}
def create_capital_graph(df, target, title):
return {
'data': [dict(
x=df['month'],
y=df['cap_no_reinv'],
mode='lines',
name='Капитал - вложенные деньги'
), dict(
x=df['month'],
y=df['cap_percent_reinv'],
mode='lines',
name='Капитал - с ростом цены акций'
), dict(
x=df['month'],
y=df['cap_all_reinv'],
mode='lines',
name='Капитал - с ростом цены акций и реинвестицией дивидендов'
)],
'layout': {
'annotations': [{
'x': 0, 'y': 0, 'xanchor': 'left', 'yanchor': 'bottom',
'xref': 'paper', 'yref': 'paper', 'showarrow': False,
'align': 'left', 'bgcolor': 'rgba(255, 255, 255, 0.5)',
'text': title
}],
'yaxis': {'type': 'linear', 'title': 'Капитал, $'},
'xaxis': {'showgrid': True, 'title': 'Месяцы'}
}
}
def total_result_div(result):
if result['dividend'] <= 0:
return "Желаемая дивидендная доходность не найдена в заданном промежутке времени, попробуйте выставить больше лет для графиков"
return "Требуемый ежемесячный дивидендный результат будет достигнут через [ {} месяцев], при этом будет вложено: [ {}$ ], но ваш капитал достигнет к этому времени за счет сложного процента: [ {:.2f}$ ]".format(result['month'], result['current_capital_no_reinv'], result['current_capital_with_all_reinv'])
""" Var in input field might be not floar, but empty or string, in such case use default value """
def input_to_float(var, default):
if var.isdigit():
return float(var)
else:
return default
@app.callback(
[Output(component_id='dividends-graph',component_property='figure'),
Output(component_id='capital-graph',component_property='figure'),
Output(component_id='result', component_property='children')],
[Input(component_id='maximum-years', component_property='value'),
Input(component_id='divident-income-per-month', component_property='value'),
Input(component_id='start-capital', component_property='value'),
Input(component_id='regular-payment', component_property='value'),
Input(component_id='start-divident-percent', component_property='value'),
Input(component_id='average-cost-grow-percent', component_property='value')]
)
def update_output_div(maximum_years, divident_income_per_month, start_capital, regular_payment, start_divident_percent, average_cost_grow_percent):
max_years_number = int(maximum_years)
MONTH_COUNT_YEAR = 12
divident_income_per_month = input_to_float(divident_income_per_month, default__divident_income_per_month)
start_capital = input_to_float(start_capital, default__start_capital)
regular_payment = input_to_float(regular_payment, default__regular_payment)
start_divident_percent = input_to_float(start_divident_percent, default__start_divident_percent)
average_cost_grow_percent = input_to_float(average_cost_grow_percent, default__average_cost_grow_percent)
current_capital_no_reinv = current_capital_just_percent_reinv = current_capital_with_all_reinv = start_capital
info = {}
info['month'] = []
info['dividend_no_reinv'] = []
info['dividend_percent_reinv'] = []
info['dividend_all_reinv'] = []
info['cap_no_reinv'] = []
info['cap_percent_reinv'] = []
info['cap_all_reinv'] = []
target = {'month': 0, 'dividend': 0, 'current_capital_no_reinv': 0, 'current_capital_with_all_reinv': 0}
for month in range(max_years_number * MONTH_COUNT_YEAR):
info['month'].append(month)
current_capital_no_reinv += regular_payment
info['cap_no_reinv'].append(current_capital_no_reinv)
current_dividend_no_reinv = current_capital_no_reinv * start_divident_percent / 100 / MONTH_COUNT_YEAR
info['dividend_no_reinv'].append(current_dividend_no_reinv)
current_capital_just_percent_reinv = (current_capital_just_percent_reinv + regular_payment) * (1 + average_cost_grow_percent / 100 / MONTH_COUNT_YEAR)
info['cap_percent_reinv'].append(current_capital_just_percent_reinv)
current_dividend_just_percent_reinv = current_capital_just_percent_reinv * start_divident_percent / 100 / MONTH_COUNT_YEAR
info['dividend_percent_reinv'].append(current_dividend_just_percent_reinv)
current_dividend_with_all_reinv = current_capital_with_all_reinv * start_divident_percent / 100 / MONTH_COUNT_YEAR
info['dividend_all_reinv'].append(current_dividend_with_all_reinv)
current_capital_with_all_reinv = (current_capital_with_all_reinv + regular_payment + current_dividend_with_all_reinv) * (1 + average_cost_grow_percent / 100 / MONTH_COUNT_YEAR)
info['cap_all_reinv'].append(current_capital_with_all_reinv)
if((current_dividend_with_all_reinv >= divident_income_per_month) and not (target['month'])):
target['month'] = month
target['dividend'] = current_dividend_with_all_reinv
target['current_capital_no_reinv'] = current_capital_no_reinv
target['current_capital_with_all_reinv'] = current_capital_with_all_reinv
return create_dividends_graph(info, target, ''), create_capital_graph(info, target, ''), total_result_div(target)
Перезапускаем сервис, чтобы применить все изменения на веб сайте
# systemctl restart invests
Работа дивидендного калькулятора
Значения по-умолчанию для основных входных данных:
желаемый ежемесячный доход от дивидендов — 1000 долларов
стартовый капитал — 1000 долларов
ежемесячные пополнения брокерского счета для покупки дивидендных акций — 1000 долларов
средний процент дивидендов по акциям — 4%
средний рост цены дивидендных акций — 12.5% (на основе статистики по S&P Dividends aristocrats)
Все входные данные размещаются в редактируемых полях
Результаты можно увидеть на графиках ниже, куда поступают данные после обработки
график роста ежемесячных дивидендов:
a) получаемый за счет роста цены акций и ежемесячных ре-инвестирований дивидендов,
б) получаемый только за счет роста цены акций, т.е. если дивиденды будем выводить (без ре-инвестирования),
в) получаемый, если бы мы просто клали деньги «под подушку», без инвестирования, вообще
график роста капитала при тех же трех, рассматриваемых в первом пункте, условиях (а), б), в))
Бонусом, над графиками, представлен «ползунок» взгляда в будущее (изменяется в месяцах): что же произойдет с нашими дивидендами и капиталом, если продолжать вкладывать деньги следуя той же стратегии и далее
Итог
При взгляде на 2 графика: роста дивидендного дохода и роста капитала, можно сделать несколько интересных выводов:
значительно выгоднее вкладывать деньги в хорошие компании, чем просто хранить их
значительно выгоднее ре-инвестировать дивиденды
особенно, эффект заметен на горизонте более 10 лет
рост происходит экспоненциально
цифры по прошествии 20 лет и больше, даже, пугают
другие выводы можете сделать самостоятельно, «поиграв» с настройками, входными параметрами
Предположим у нас есть сконфигурированный сервер, готовый к запуску веб-сайта (для Caddy, Nginx и Apache настраивали ранее) и так же git репозиторий (GitHub или Bitbucket) c кодом, который нужно «деплоить» в подготовленную директорию для сайта.
В данной статье будем использовать метод аутентификации на Git с помощью публичного SSH ключа.
Установим git, если ранее он не был установлен
$ sudo yum install git -y
Далее, сгенерируем SSH ключи, получаем root-права
$ sudo su
На целевом сервере переходим в домашнюю директорию
# cd ~/.ssh
И проверяем есть ли пара файлов с именем id_dsa — выводим список всех файлов в директории
# ls
Если их нету — необходимо сгенерировать. Команда для генерации ключей
# ssh-keygen -t rsa
Оставляем имя по-умолчанию (id_dsa), далее, система попросит ввести passphrase (оставляем пустым) и повторить ввод passphrase (тоже пустое)
В результате, в папке .ssh (внутри домашней директории) будет создана пара ключей — приватный и публичный (который с расширением .pub)
Распечатываем его (в нашем примере имя — gitkey.pub)
# cat ~/.ssh/id_dsa.pub
И копируем содержимое в буфер обмена для сохранения в настройках аутентификации репозитория по SSH
Давайте рассмотрим этот процесс на примере Bitbucket
Выбираем репозиторий на сайте Bitbucket, затем Repository settings — Access keys и жмем кнопку Add key
Label — даем название ключу
Key — вставляем наш публичный ключ из буфера и сохраняем — Add key
Возвращаемся к серверу, переходим к папке с проектами
# cd /var/www
И клонируем код из репозитория (в нашем примере имя репозитория domain-project) в рабочую папку для проекта (в нашем примере это папка domain-web)
Чтобы выписать сертификат, letsencrypt проверяет принадлежит ли вам домен, и делает это с помощью временных файлов, которые генерирует на сервере и проверяет их доступность
Так же нам понадобится комбинированный сертификат — cert + dhparam, который мы генерировали ранее (не забываем поменять your-domain.com на ваш домен в путях ниже)
Бонус: если вы внесли изменения в код проекта и слили в репозиторий, теперь очень просто подтянуть изменения на сервере. Примерный подход будет выглядеть так
Заходим под рутом
$ sudo su
Переходим в папку проекта
# cd /var/www/your-project-name
Переключаемся на мастер-ветку
# git checkout .
# git reset
Следующий шаг опциональный — не рекомендуется делать, если у вас не добавлены в гит-игнор все ресурсы, который могут на сервере отличаться от локальной машины (например папки vendor или uploads). Тут очищаются все файлы, которые не входят в структуру репозитория