parent
68af2939a3
commit
487ce22cfc
|
@ -42,6 +42,6 @@ dump.rdb
|
|||
fwup-key.priv
|
||||
.env
|
||||
# secret config stuffs for dev environment.
|
||||
config/auth_secret.exs
|
||||
auth_secret.exs
|
||||
flash_fw.sh
|
||||
tmp
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
# TODO
|
||||
|
||||
## Serial
|
||||
- [ ] FW updates.
|
||||
- [ ] Autodetect TTY. (Or use Configuration?)
|
||||
|
||||
## System
|
||||
- [ ] OS updates.
|
||||
- [ ] Key rotation for signed releases.
|
||||
|
||||
|
||||
## Configuration
|
||||
- [ ] Rewrite `configurator` web app.
|
||||
- [ ] Only have configuration router up during configuration time.
|
||||
|
||||
## CeleryScript
|
||||
- [ ] Implement _EVERY_ CS Node again.
|
||||
|
||||
## Logging
|
||||
- [ ] Reimplement Logger
|
||||
|
||||
## HTTP
|
||||
- [ ] Write tests for HTTP.
|
||||
|
||||
## Debug
|
||||
- [ ] Implement `debug`/`developer` mode.
|
|
@ -26,36 +26,24 @@ config :fs, path: "/tmp/images"
|
|||
|
||||
# Configure your our system.
|
||||
# Default implementation needs no special stuff.
|
||||
config :farmbot, :init, [
|
||||
Farmbot.Bootstrap.Configurator
|
||||
]
|
||||
# See Farmbot.System.Supervisor and Farmbot.System.Init for details.
|
||||
config :farmbot, :init, []
|
||||
|
||||
# Transports.
|
||||
config :farmbot, :transport, [
|
||||
Farmbot.BotState.Transport.GenMqtt
|
||||
]
|
||||
config :farmbot, :transport, []
|
||||
|
||||
|
||||
# Configure Farmbot Behaviours.
|
||||
config :farmbot, :behaviour, [
|
||||
authorization: Farmbot.Bootstrap.Authorization,
|
||||
|
||||
# uncomment this line if you have a real arduino plugged in. You will also need
|
||||
# ensure the config for `:uart_handler` is correct.
|
||||
# firmware_handler: Farmbot.Firmware.UartHandler,
|
||||
firmware_handler: Farmbot.Host.FirmwareHandlerStub,
|
||||
system_tasks: Farmbot.Host.SystemTasks
|
||||
]
|
||||
|
||||
config :farmbot, :uart_handler, [
|
||||
tty: "/dev/ttyACM0"
|
||||
]
|
||||
|
||||
config :nerves_firmware_ssh,
|
||||
authorized_keys: [
|
||||
File.read!(Path.join(System.user_home!, ".ssh/id_rsa.pub"))
|
||||
]
|
||||
|
||||
config :bootloader,
|
||||
init: [:nerves_runtime, :nerves_init_gadget],
|
||||
app: :farmbot
|
||||
case target do
|
||||
"host" -> import_config("host/#{env}.exs")
|
||||
_ ->
|
||||
if File.exists?("config/#{target}/#{env}.exs") do
|
||||
import_config("#{target}/#{env}.exs")
|
||||
else
|
||||
import_config("target/#{env}.exs")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
use Mix.Config
|
||||
|
||||
# You should copy this file to config/host/auth_secret.exs
|
||||
# And make sure to configure the credentials to something that makes sense.
|
||||
|
||||
config :farmbot, :authorization, [
|
||||
email: "admin@admin.com",
|
||||
password: "password123",
|
||||
server: "http://localhost:3000"
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
use Mix.Config
|
||||
|
||||
# You should copy this file to config/host/auth_secret.exs
|
||||
# And make sure to configure the credentials to something that makes sense.
|
||||
|
||||
config :farmbot, :authorization, [
|
||||
email: "admin@admin.com",
|
||||
password: "password123",
|
||||
server: "http://localhost:3000",
|
||||
token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbkBhZG1pbi5jb20iLCJpYXQiOjE1MDUwMDM1ODMsImp0aSI6ImIxZTM4ODJmLWJjYzMtNDcxNS04NTViLWVlZjBiZGQ0MTY0YSIsImlzcyI6Ii8vMTAuMTg2LjE5MC45MDozMDAwIiwiZXhwIjoxNTA4NDU5NTgzLCJtcXR0IjoiMTAuMTg2LjE5MC45MCIsIm9zX3VwZGF0ZV9zZXJ2ZXIiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2Zhcm1ib3QvZmFybWJvdF9vcy9yZWxlYXNlcy9sYXRlc3QiLCJmd191cGRhdGVfc2VydmVyIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9GYXJtYm90L2Zhcm1ib3QtYXJkdWluby1maXJtd2FyZS9yZWxlYXNlcy9sYXRlc3QiLCJib3QiOiJkZXZpY2VfMiJ9.kqsn8PIfReYRKlvqaIYsMZ5ai2TUP66ToqUX28V0Yt0xG7hY4eqBkLw8PWWgvCcx3J-xGr4wAKUdclkex-yZVU9L912eqFSrYSIkBKO4qUCS5Xtvc6MpdwI34r-JQckfoCs_G_PtTI1YNR8Rah8UyI4U8raIh_hjUmk5X0aWYxOqC9FgGfhtnR62RsYr1DzAKtf211g80VFUbQmE-9mIKBEVjPv5oiSzRo_FXovn_JaqG9JQkP-sabV4P9ItksJ9LNzwIZ_EYJCzM-838Ad6507CpbcpbVC6cN_WWKU0vP4L2Akvc4_0QLZ3TeY8UhgYqasPwcpOmvmzbyEJRFmZ1w"
|
||||
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
use Mix.Config
|
||||
unless File.exists?("config/host/auth_secret.exs") do
|
||||
Mix.raise("You need to configure your dev environment. See `config/host/auth_secret.exs` for an example.\r\n")
|
||||
end
|
||||
|
||||
import_config("auth_secret.exs")
|
||||
|
||||
# Configure your our system.
|
||||
# Default implementation needs no special stuff.
|
||||
config :farmbot, :init, [
|
||||
]
|
||||
|
||||
# Transports.
|
||||
config :farmbot, :transport, [
|
||||
Farmbot.BotState.Transport.GenMqtt
|
||||
]
|
||||
|
||||
|
||||
# Configure Farmbot Behaviours.
|
||||
config :farmbot, :behaviour, [
|
||||
authorization: Farmbot.Bootstrap.Authorization,
|
||||
|
||||
# uncomment this line if you have a real arduino plugged in. You will also need
|
||||
# ensure the config for `:uart_handler` is correct.
|
||||
# firmware_handler: Farmbot.Firmware.UartHandler,
|
||||
firmware_handler: Farmbot.Host.FirmwareHandlerStub,
|
||||
system_tasks: Farmbot.Host.SystemTasks
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
use Mix.Config
|
||||
|
||||
unless File.exists?("config/host/auth_secret_test.exs") do
|
||||
Mix.raise("You need to configure your test environment.\r\n")
|
||||
end
|
||||
import_config("auth_secret_test.exs")
|
||||
|
||||
config :farmbot, :init, []
|
||||
|
||||
# Transports.
|
||||
config :farmbot, :transport, []
|
||||
|
||||
|
||||
# Configure Farmbot Behaviours.
|
||||
config :farmbot, :behaviour, [
|
||||
authorization: Farmbot.Test.Authorization,
|
||||
firmware_handler: Farmbot.Host.FirmwareHandlerStub,
|
||||
system_tasks: Farmbot.Test.SystemTasks
|
||||
]
|
|
@ -0,0 +1,33 @@
|
|||
use Mix.Config
|
||||
|
||||
# Configure your our system.
|
||||
# Default implementation needs no special stuff.
|
||||
config :farmbot, :init, [
|
||||
# Farmbot.Bootstrap.Configurator
|
||||
]
|
||||
|
||||
# Transports.
|
||||
config :farmbot, :transport, [
|
||||
Farmbot.BotState.Transport.GenMqtt
|
||||
]
|
||||
|
||||
|
||||
# Configure Farmbot Behaviours.
|
||||
config :farmbot, :behaviour, [
|
||||
authorization: Farmbot.Bootstrap.Authorization,
|
||||
firmware_handler: Farmbot.Firmware.UartHandler,
|
||||
system_tasks: Farmbot.Host.SystemTasks
|
||||
]
|
||||
|
||||
config :farmbot, :uart_handler, [
|
||||
tty: "/dev/ttyACM0"
|
||||
]
|
||||
|
||||
config :nerves_firmware_ssh,
|
||||
authorized_keys: [
|
||||
File.read!(Path.join(System.user_home!, ".ssh/id_rsa.pub"))
|
||||
]
|
||||
|
||||
config :bootloader,
|
||||
init: [:nerves_runtime, :nerves_init_gadget],
|
||||
app: :farmbot
|
|
@ -0,0 +1,9 @@
|
|||
use Mix.Config
|
||||
import_config("dev.exs")
|
||||
|
||||
config :nerves_firmware_ssh,
|
||||
authorized_keys: []
|
||||
|
||||
config :bootloader,
|
||||
init: [:nerves_runtime],
|
||||
app: :farmbot
|
|
@ -72,4 +72,5 @@ defmodule Farmbot.Bootstrap.Authorization do
|
|||
end
|
||||
|
||||
defp handle_error({:error, _reason} = error), do: error
|
||||
defp handle_error({:error, :invalid, _} = error), do: error
|
||||
end
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
defmodule Farmbot.Bootstrap.Configurator do
|
||||
use GenServer
|
||||
def start_link(_, opts) do
|
||||
creds = [
|
||||
email: "connor@farmbot.io",
|
||||
password: "password123",
|
||||
server: "https://staging.farmbot.io"
|
||||
]
|
||||
Application.put_env(:farmbot, :authorization, creds)
|
||||
:ignore
|
||||
end
|
||||
end
|
|
@ -98,11 +98,12 @@ defmodule Farmbot.Bootstrap.Supervisor do
|
|||
end
|
||||
end
|
||||
|
||||
defp actual_init(_args, email, pass, server) do
|
||||
Logger.info "Beginning authorization: #{email} - #{server}"
|
||||
defp actual_init(args, email, pass, server) do
|
||||
Logger.info "Beginning authorization: #{@auth_task} - #{email} - #{server}"
|
||||
# get a token
|
||||
case @auth_task.authorize(email, pass, server) do
|
||||
{:ok, token} ->
|
||||
Logger.info "Successful authorization: #{@auth_task} - #{email} - #{server}"
|
||||
children = [
|
||||
supervisor(Farmbot.BotState.Supervisor, [token, [name: Farmbot.BotState.Supervisor ]]),
|
||||
supervisor(Farmbot.HTTP.Supervisor, [token, [name: Farmbot.HTTP.Supervisor]]),
|
||||
|
@ -113,6 +114,9 @@ defmodule Farmbot.Bootstrap.Supervisor do
|
|||
# an application start fail and we would factory_reset from there,
|
||||
# the error message is just way more useful here.
|
||||
{:error, reason} -> Farmbot.System.factory_reset(reason)
|
||||
# If we got invalid json, just try again.
|
||||
# FIXME(Connor) Why does this happen?
|
||||
{:error, :invalid, _} -> actual_init(args, email, pass, server)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
defmodule Farmbot.Firmware.Handler do
|
||||
@moduledoc "Handler for a firmware."
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
#!/bin/bash
|
||||
SYSTEM=$1
|
||||
export MIX_ENV=prod
|
||||
export MIX_TARGET=$SYSTEM
|
||||
echo "building firmware for $SYSTEM"
|
||||
npm install
|
||||
npm run build
|
||||
mix deps.get
|
||||
mix firmware
|
|
@ -1,23 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Collects all the various images and archives for constructing a release.
|
||||
SYSTEM=$1
|
||||
VERSION=$2
|
||||
export MIX_ENV=prod
|
||||
export MIX_TARGET=$SYSTEM
|
||||
REL_DIR=release-$VERSION
|
||||
mkdir -p $REL_DIR
|
||||
|
||||
FIRM_FILE=images/$MIX_ENV/$SYSTEM/farmbot.fw # the .fw file generated by `mix firmware`
|
||||
FIRM_FILE_REL=$REL_DIR/farmbot-$SYSTEM-$VERSION.fw # where we want our formatted system
|
||||
FIRM_FILE_REL_IMG=$REL_DIR/farmbot-$SYSTEM-$VERSION.img # same as the above file, but a .img file
|
||||
|
||||
SYSTEM_DIR=nerves/NERVES_SYSTEM_$SYSTEM # the NERVES_SYSTEM build dir
|
||||
SYSTEM_TAR=$SYSTEM_DIR/nerves_system_$SYSTEM.tar.gz # the archive generated by `make system`
|
||||
SYSTEM_TAR_REL=$REL_DIR/farmbot.rootfs-$SYSTEM-$VERSION.tar.gz # where we want our outputted archive to be
|
||||
|
||||
# copy the firmware file to the release file
|
||||
cp $FIRM_FILE $FIRM_FILE_REL
|
||||
# build an image from the release firmware
|
||||
fwup -a -d $FIRM_FILE_REL_IMG -i $FIRM_FILE_REL -t complete
|
||||
# copy the rootfs archive to the release dir
|
||||
cp $SYSTEM_TAR $SYSTEM_TAR_REL
|
|
@ -1,6 +0,0 @@
|
|||
#!/bin/bash
|
||||
SYSTEM=$1
|
||||
echo "building system for $SYSTEM"
|
||||
cd nerves/NERVES_SYSTEM_$SYSTEM
|
||||
time make
|
||||
make system
|
|
@ -1,24 +0,0 @@
|
|||
#!/bin/bash
|
||||
SYSTEM=$1
|
||||
NERVES_SYSTEM_BR_GIT="https://github.com/nerves-project/nerves_system_br"
|
||||
NERVES_SYSTEM_BR_COMMIT="049d8e19b69b0f84084182d9bdd915e4eb431ed5"
|
||||
|
||||
if [ -d "nerves/NERVES_SYSTEM_$SYSTEM" ]; then
|
||||
# Control will enter here if $DIRECTORY exists.
|
||||
echo "NERVES_SYSTEM_$SYSTEM dir found."
|
||||
else
|
||||
echo "NERVES_SYSTEM_$SYSTEM dir not found."
|
||||
CWD=$(pwd)
|
||||
git clone $NERVES_SYSTEM_BR_GIT nerves/nerves_system_br
|
||||
cd nerves/nerves_system_br
|
||||
git checkout $NERVES_SYSTEM_BR_COMMIT
|
||||
cd $CWD
|
||||
fi
|
||||
|
||||
nerves/nerves_system_br/create-build.sh nerves/nerves_system_$SYSTEM/nerves_defconfig nerves/NERVES_SYSTEM_$SYSTEM
|
||||
if [ ! -f nerves/nerves_system_br/buildroot/.farmbot_applied_patches_list ]; then
|
||||
echo "Applying FarmbotOS patches"
|
||||
nerves/nerves_system_br/buildroot/support/scripts/apply-patches.sh nerves/nerves_system_br/buildroot patches
|
||||
cp nerves/nerves_system_br/buildroot/.applied_patches_list nerves/nerves_system_br/buildroot/.farmbot_applied_patches_list
|
||||
nerves/nerves_system_br/create-build.sh nerves/nerves_system_$SYSTEM/nerves_defconfig nerves/NERVES_SYSTEM_$SYSTEM
|
||||
fi
|
|
@ -1,117 +0,0 @@
|
|||
version = Path.join([__DIR__, "..", "VERSION"]) |> File.read! |> String.strip
|
||||
IO.puts version
|
||||
|
||||
{commitish, _} = System.cmd("git", ["log", "--pretty=format:%hQQ%adQQ%f", "-1"])
|
||||
thing = String.split(commitish, "QQ")
|
||||
|
||||
IO.puts "Finding private key."
|
||||
priv_key_path = System.get_env("PRIV_KEY_FILE") || nil
|
||||
|
||||
if priv_key_path do
|
||||
IO.puts("Found private key!")
|
||||
else
|
||||
IO.puts("Could not find private key!")
|
||||
end
|
||||
|
||||
IO.puts "Building default portions"
|
||||
initial = "# THIS FILE WAS GENERATED BY `build_makefile.exs`
|
||||
# #{Enum.join(thing, "\n# ")}
|
||||
|
||||
default: rpi3
|
||||
|
||||
ifdef SILENT
|
||||
.SILENT:
|
||||
endif
|
||||
|
||||
dev_env:
|
||||
\texport MIX_ENV=dev
|
||||
|
||||
prod_env:
|
||||
\texport MIX_ENV=prod
|
||||
|
||||
clean:
|
||||
\t$(info Cleaning)
|
||||
\trm -rf nerves/NERVES_SYSTEM_*
|
||||
\trm -rf nerves/nerves_system_br
|
||||
\trm -rf npm-debug*
|
||||
\trm -rf erl_crash.dump
|
||||
\trm -rf doc
|
||||
\trm -rf cover
|
||||
\trm -rf deps
|
||||
\trm -rf node_modules
|
||||
\trm -rf cache
|
||||
\trm -rf _build
|
||||
\trm -rf images
|
||||
\trm -rf images
|
||||
\trm -rf latest-release
|
||||
\trm Makefile
|
||||
\telixir scripts/generate_makefile.exs
|
||||
|
||||
test: dev_env
|
||||
\tscripts/run_tests.sh
|
||||
|
||||
travis_test: dev_env
|
||||
\tscripts/run_travis_tests.sh
|
||||
|
||||
## End default portion.\n"
|
||||
|
||||
build_system_part = fn(sys) ->
|
||||
"\n## begin #{sys} portion.
|
||||
|
||||
## #{sys} env
|
||||
env-#{sys}: prod_env
|
||||
\texport NERVES_TARGET=#{sys}
|
||||
|
||||
## #{sys} build
|
||||
#{sys}: env-#{sys} system-#{sys} firmware-#{sys}
|
||||
\t$(info Building stuff for #{sys})
|
||||
|
||||
## #{sys} create-build
|
||||
create-build-#{sys}:
|
||||
\tscripts/clone_system.sh #{sys}
|
||||
|
||||
## #{sys} system
|
||||
system-#{sys}: create-build-#{sys}
|
||||
\t$(info Building Linux System for #{sys})
|
||||
\tscripts/build_system.sh #{sys}
|
||||
|
||||
## #{sys} firmware
|
||||
firmware-#{sys}:
|
||||
\t$(info Building Firmware for #{sys})
|
||||
\tscripts/build_firmware.sh #{sys}
|
||||
|
||||
release-#{sys}: #{sys}
|
||||
\tscripts/build_release_images.sh #{sys} #{version}
|
||||
#{if priv_key_path, do: "\tscripts/sign_release.sh #{sys} #{version} #{priv_key_path}"}
|
||||
|
||||
## end #{sys} portion."
|
||||
end
|
||||
|
||||
list = File.ls!("nerves")
|
||||
only_systems = Enum.reduce(list, [], fn(d, acc) ->
|
||||
case d do
|
||||
# ignore nerves_system_br
|
||||
"nerves_system_br" -> acc
|
||||
"nerves_system_"<> sys -> [sys | acc]
|
||||
_ -> acc
|
||||
end
|
||||
end)
|
||||
|
||||
initial_mod = Enum.reduce(only_systems, initial, fn(sys, acc) ->
|
||||
IO.puts "Defining system: #{sys}"
|
||||
acc <> build_system_part.(sys)
|
||||
end)
|
||||
|
||||
IO.puts "Defining release for #{version}"
|
||||
final = initial_mod <>
|
||||
# "\n\n## Release will build all the systems.
|
||||
# release: clean #{Enum.map(only_systems, fn(a) -> " release-"<>a end)}"
|
||||
"\n\n## Release will build all the systems.
|
||||
release: clean release-rpi3"
|
||||
|
||||
IO.puts "Writing file."
|
||||
File.write("Makefile", final)
|
||||
blah = "release-#{version}"
|
||||
IO.puts blah
|
||||
File.rm "./release-latest"
|
||||
File.ln_s(blah, "./release-latest")
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/bash
|
||||
export MIX_ENV=test
|
||||
mix deps.get
|
||||
mix all_test
|
|
@ -1,7 +0,0 @@
|
|||
#!/bin/bash
|
||||
export MIX_ENV=test
|
||||
mix deps.get
|
||||
mix travis_test || { echo 'Tests failed!' ; exit 1; }
|
||||
if [[ "$TRAVIS_BRANCH" == "staging" ]]; then
|
||||
SILENT=true make release
|
||||
fi
|
|
@ -1,11 +0,0 @@
|
|||
#!/bin/bash
|
||||
SYSTEM=$1
|
||||
VERSION=$2
|
||||
PRIV_KEY_PATH=$3
|
||||
REL_DIR=release-$VERSION
|
||||
FIRM_FILE_REL=$REL_DIR/farmbot-$SYSTEM-$VERSION.fw
|
||||
SIGNED_FIRM_FILE_REL=$REL_DIR/farmbot-$SYSTEM-$VERSION-signed.fw
|
||||
|
||||
MIX_ENV=prod MIX_TARGET=$SYSTEM mix firmware.sign $PRIV_KEY_PATH $SIGNED_FIRM_FILE_REL
|
||||
mv $FIRM_FILE_REL $FIRM_FILE_REL.unsigned
|
||||
mv $SIGNED_FIRM_FILE_REL $FIRM_FILE_REL
|
|
@ -1,12 +0,0 @@
|
|||
#!/bin/sh
|
||||
CWD=`pwd`
|
||||
mkdir /tmp/tty0tty
|
||||
cd /tmp/tty0tty
|
||||
git clone https://github.com/freemed/tty0tty .
|
||||
cd module
|
||||
make
|
||||
sudo cp tty0tty.ko /lib/modules/$(uname -r)/kernel/drivers/misc/
|
||||
sudo depmod
|
||||
sudo modprobe tty0tty
|
||||
sudo chmod 666 /dev/tnt*;
|
||||
cd $CWD
|
|
@ -1,6 +1,7 @@
|
|||
defmodule FarmbotTestSupport do
|
||||
@moduledoc "Test Helpers."
|
||||
import Farmbot.DebugLog, only: [color: 1]
|
||||
require Logger
|
||||
|
||||
defp error(err) do
|
||||
"""
|
||||
|
@ -16,6 +17,7 @@ defmodule FarmbotTestSupport do
|
|||
end
|
||||
|
||||
def preflight_checks do
|
||||
Logger.info "Starting Preflight Checks."
|
||||
with {:ok, tkn} <- ping_api(),
|
||||
:ok <- ping_mqtt(tkn) do
|
||||
:ok
|
||||
|
@ -28,14 +30,18 @@ defmodule FarmbotTestSupport do
|
|||
server = Application.get_env(:farmbot, :authorization)[:server]
|
||||
email = Application.get_env(:farmbot, :authorization)[:email]
|
||||
password = Application.get_env(:farmbot, :authorization)[:password]
|
||||
Logger.info "Preflight check: api: #{server}"
|
||||
case Farmbot.Bootstrap.Authorization.authorize(email, password, server) do
|
||||
{:error, _reason} -> :api
|
||||
{:ok, tkn} -> {:ok, tkn}
|
||||
{:ok, tkn} ->
|
||||
Logger.info "Preflight check api complete."
|
||||
{:ok, tkn}
|
||||
end
|
||||
end
|
||||
|
||||
defp ping_mqtt(tkn) do
|
||||
url = Farmbot.Jwt.decode!(tkn).mqtt
|
||||
Logger.info "Preflight check: mqtt: #{url}"
|
||||
case :gen_tcp.connect(to_charlist(url), 1883, [:binary]) do
|
||||
{:error, _} -> :mqtt
|
||||
{:ok, port} -> :gen_tcp.close(port)
|
||||
|
|
Loading…
Reference in New Issue